How to check the type of an operation in a statement?

Viewed 328

I want to be able to check if the type of the return value is the same as the type of a method in ANTLR. (i.e int processOperation() should return an int like return (3-1*4))

My grammar is the following: https://github.com/RodrigoZea/Lab00DDC/blob/fda787998e5ed1cc5e5d94e6506ed6ca08dbd955/Decaf/Decaf.g4

I'm using the python implementation of ANTLR4, but I'm unsure as to how to check the type of an operation in a return statment, for example (1+3*4) should return an int. I'm using a Listener, so my logic is as follows:

  1. First check the value if its a primitive (i.e. return "random", return 1)
  2. Check if the value is an operation or a single variable.

For a single variable, searching it up in the symbol table would be enough, but for an operation I'm unsure on how to approach it. I've read about using a ParseTreeProperty<> but I don't think there's an implementation of that in the Python version of ANTLR4, that seemed to be the best approach from what I've read in the ANTLR4 definitive reference since it will save the nodes' (and the operation subtree) data type and I can easily check its type and compare it to my method type. I'm guessing I would need to check when I'm entering an operator rule, but I'm unsure on what to do with that data or if there's a way to implement a ParseTreeProperty in Python. Thanks.

2 Answers

ParseTreeProperty is a convenience for "attaching" properties to nodes of your parse tree, and could be a useful way to keep track of the type of each node in your tree. However, as the comments mention, there are other data structures you can you to track the type of each node and map back to it. (Note: if you use this approach with a listener, as your question implies, you'd need to implement it in the *Exit() method, as you would want all the children to have been "listened to" and their types assigned, so that you can determine the type of the parent expression.)

Using a listener, you can also just have a stack of types. When you exit each expression, it pops the types of all of its children, evaluates the expression type for itself, and pushes that type on the stack. You, of course, have to take care to properly manage to pushing and popping (look out for exceptions), but it can be a reasonably clean implementation.

You could also implement an expression type validation visitor. With this approach, you write an expression visitor that returns it's type. With each overriden visit*() you can just call visit() on each child to get it's type, and then decide what you want to resulting type to be (and probably whether it's even a valid expression). Notice that ```visit``ing a node return a result with visitors, this is one of the key differences between visitors and listeners (the other being, that, with visitors, you ave to explicitly choose how to navigate your child nodes).

So far as "what to do with this data", at this point you're making design decisions about how you want your language to behave, what's valid, etc.

For example:

7 * "string"

Maybe you decide 7 is an Int type and "string" is a String type. In your listener/visitor for for multiplication expressions, it's up to you to decide if this is an error (and the resulting "type" is InvalidType, perhaps), or maybe, like Ruby, it's a cute way of getting "stringstringstringstringstringstringstring", in which case you'd return a type of String. For functions you have decisions to make about the return type of the function. Do you require them to be explicitly defined? Must the be defined before they're referenced (if not, you'll need to make a pass of you parse tree creating a symbol table of functions and return types to reference, before you can navigate your tree evaluating expression types). Maybe, you have a dynamic language where different input types (or even values) might result in different return types from your function.

Clearly, this gets pretty deep into language design choices, and languages have made many different decisions about how to handle them. ANTLR is just your parsing technology and (other than providing convenience classes like listeners and visitors) has nothing to say about how you make these decisions or how you implement them. And, there's not a way to codify them in your grammar as they ares semantic concerns that have no impact on parsing or the construction of your parse tree.

So, basing myself on what Mike and kaby answered, I came up with a solution. It's incredibly simple but very functional. The best way to replicate a ParseTreeProperty in Python is to create a dictionary, the ctx Object will be the key and the value is set manually, depending on what you want the value to be (this is where Mike's answer comes in handy). To update the dictionary values, you will do this on the *Exit() methods, just as Mike said as well.

For example, if you're exiting an int literal, char literal, or whatever (you can take my grammar as reference) you can add an entry to your dictionary as follows:

    def exitType_literal(self, ctx: DecafParser.Type_literalContext):
        self.nodeTypes[ctx] = 'type'

So for example, if I wanted to save a node as an int value, I would do something like...

    def exitInt_literal(self, ctx: DecafParser.Int_literalContext):
        self.parseTreePropertyDictionary[ctx] = 'int'

If you want to get the value of a variable however, you would have to search it up on your implementation of a Symbol Table. That was my approach on getting the type value.

So once every node is setup, you can simply setup how you want to process your operations. For example, if you want a "+" operator to be with ints, you would check the type of the first and second operator on your dictionary, check if both are ints, and if thats the case, then save it on your dictionary as an 'int' type node where you are processing your "+" operator.

Then, to get the type of the operation, you will simply access your dictionary on that node and it will return 'int' or whatever the type you set it up to be.

Related