Intro
In this series I am creating a transpiler from Python to Golang called Pogo.
In the last post I thought we were mostly finished with functions, then I forgot that most of the time we want to return a value from a function. Not having this functionality seems kind of like cheating, so we're going to implement that right now.
Function Typing
Lucky for us, return types are available in Python's type annotations, so we can add that in.
def test(x: int, y: int) -> None:
And we also have to add None
because we forgot to add that in (my bad). So now we have 2 tokens to add, None
, and ->
. Furthermore, we have to add return statements (which I think is mostly done), and the ability to use calls as any literal would be able to be used.
Lexing
Adding None
won't be that hard in the lexer, neither will the arrow.
if word == "None" {
token = Token{tokenCode["L_NULL"], word, l.line}
}
else if l.curChar == '-' {
if l.peek() == '>' {
l.nextChar()
token = Token{tokenCode["ARROW"], "->", l.line}
} else {
token = Token{tokenCode["MO_SUB"], "-", l.line}
}
}
Of course, we have to make sure that we are looking at an arrow, and not just the subtract sign.
Parsing
In the parser, it should be mandatory to express what type a function returns (void functions return None
). We currently only need to change the definition. The arrow and type appear before the colon and after the right bracket, so we need to add these lines in-between the checking for these tokens.
temps, err := p.checkTokenRange([]string{
"R_PAREN",
"ARROW",
})
if err != nil {
return s, err
}
s.children = append(s.children, temps...)
p.nextToken()
temp, err = p.checkTokenChoices([]string{
"IDENTIFIER",
"L_NULL",
})
if err != nil {
return s, err
}
s.children = append(s.children, temp)
p.nextToken()
Modularizing Calls
Since we are going to be using calls in more places than one we should make the logic for calls another function, similar to if
statements. This is simply a copy paste job.
Using Calls
Now that we have modularized calls we can start using them in places such as declarations. We're going to have to work on the parser for this one.
p.setMarker()
temp, err := p.call()
if err != nil {
p.gotoMarker()
temp, err = p.checkTokenChoices([]string{
"L_BOOL",
"L_INT",
"L_STRING",
})
if err != nil {
return s, err
}
}
s.children = append(s.children, temp)
We're making great use of setMarker
here. Now we need to fix our semantic analyzer, as we're getting an error whenever we use a call here.
Semantic Analysis
First I had to give Function
s a varType
, so we can keep our types in check (print
can be given the None
type). If you remember from last post we made a bit of a switch statement to deal with what could be in the call if the current Structure
isn't an identifier. Calls start with a function name, which is different from an identifier, so we need to make a new case for that, and well, deal with it. I accidentally fixed it for declarations (without type-checking) so now we are going to put calls in calls.
case structureCode["ST_CALL"]:
var insideCall Function
name := s.children[i].children[1].text
var valid bool
for i := 0; i < len(funcs); i++ {
valid = false
if name == funcs[i].name {
valid = true
insideCall = funcs[i]
break
}
}
if !valid {
return createError([]string{"analyze.go", "analyze:ST_CALL"}, "An attempt to call the non-existent function ""+s.children[0].text+"" was made", s.line)
}
if fn.params[pIndex] != insideCall.varType {
return createError([]string{"analyze.go", "analyze:ST_CALL"}, """+s.children[0].text+"" is the wrong type, expected "+fn.params[pIndex]+" got "+insideCall.varType, s.line)
}
We first have to check whether we are making a valid call, then we can type check, which turned out to be pretty simple.
Back In The Parser
Of course, because of how I've done this we're back in the parser so we can implement calls in calls.
if p.peek().code == tokenCode["L_PAREN"] { // We're dealing with a call
temp, err = p.call()
if err != nil {
return s, err
}
}
Test Case
Let's give our compiler some Python, and see what it manages to do.
from GoType import *
def printNum(x: int) -> None:
print(x)
def someNum() -> int:
return 7
printNum(someNum())
Here we are just printing the number 7 but in a complicated way to test our compiler, and here is what it managed to get from it.
package main
func printNum(x int) { println(x) }
func someNum() int { return 7 }
func main() {
printNum(someNum())
}
That's pretty good, except for the formatting though, as always (did gofmt
really just set the blocks of printNum
and someNum
to the same column??). Now we have to do a small error check of types, and we don't get the result we want. If we change the type someNum
returns to string
and change nothing else we don't get a compiler error, which leads to incorrect Go code, so we definitely need to be fixing this. The issue to this was honestly just a few bugs from my bad code, so nothing to hard to fix.
Next
I know I said something about classes in the last post but then I realized that I still had quite a bit of work to do on functions, and with it being really close to the end of the month it definitely isn't feasible to add classes now. I honestly don't know what to add, I don't think an optimizer is a good idea because it doesn't create Go from Python as exactly, which is what we want. I do have to do type checking for calls in declarations still, and their use in expressions. Furthermore, comparisons should be able to be a single literal or identifier, which would allow code such as if valid:
. I think small clean ups will be the theme of the next post, just to make the compiler as a whole a bit more usable, instead of adding a bunch of new features.
Top comments (0)