Intro
In this series I am creating a transpiler from Python to Golang called Pogo. In the last post we made it possible to use parameters in functions. This was a great addition, but we weren't able to properly use it because we aren't able to call functions with parameters.
Current Error
Currently, the error we are getting about this is coming from the parser. Now the error only occurs when we try to use literals in the function, so we need to change it to allow them along with identifiers.
temp, err = p.checkTokenChoices([]string{
"IDENTIFIER",
"L_BOOL",
"L_INT",
"L_STRING",
})
for err == nil {
s.children = append(s.children, temp)
p.nextToken()
temp, err = p.checkToken("SEP")
if err != nil {
break
}
s.children = append(s.children, temp)
p.nextToken()
temp, err = p.checkTokenChoices([]string{
"IDENTIFIER",
"L_BOOL",
"L_INT",
"L_STRING",
})
}
This code now allows for any literal or identifier.
We Need Type Checking
Everything is great so far, but right now I can put a string where a function expects an integer. Since this is checking whether the code makes sense we are going to do this in the semantic analyzer. Currently we keep track of what variables are available to use in scope, and we can add a similar feature for functions. First we need to create a struct to store this information.
The Functions
type Function struct {
name string
params []string
}
The name
property holds the name, which can be used to see if we are even allowed to call the function here. Then, params
holds the types of the parameters. We don't need to hold the names of the parameters, just the types.
Scoping
Now we can pass a slice of these Function
s to analyze
.
func (a *Analyzer) analyze(s Structure, vars []Variable, funcs []Function) error
Now when we come across a function declaration, we add it to funcs
. After this we can start working on the logic of calling a function.
First, we check the function exists.
name := s.children[0].text
var valid bool
for i := 0; i < len(funcs); i++ {
valid = false
if name == funcs[i].name {
valid = true
break
}
}
if !valid {
return createError([]string{"analyze.go", "analyze:ST_CALL"}, "An attempt to call a non-existent function was made", s.line)
}
Now we can start checking each parameter's type, but first we check that they're all valid variable names (that is, if they're not literals). I lost how I fixed this because of all the weird coding over multiple files, figured out my issue was that I wasn't implying that print
was a valid function. But this does mean I have to add another type, the any
type, since print
can take in pretty much anything. This actually turned out to be pretty simple, a one line fix.
if variable.varType != fn.params[pIndex] && fn.params[pIndex] != "any" {
return createError([]string{"analyze.go", "analyze:ST_CALL"}, "Wrong type used in function call", s.line)
}
By using and we can override if the type is any.
Working Out Literals
Here is the current code for when we call a function with literals.
if s.children[i].code != structureCode["IDENTIFIER"] {
continue
}
We just skip over for now, but we don't want to do that, we want to implement it correctly. We have three types in the compiler, string
, int
, and bool
, so we are going to have cases for all three of those. If we get a string
then we only need to check whether the type is a string
, same with bool
. However, with int
we have to check all the int
types, including float
, and uint
.
if s.children[i].code != structureCode["IDENTIFIER"] {
switch s.children[i].code {
case structureCode["L_STRING"]:
if fn.params[pIndex] != "string" {
return createError([]string{"analyze.go", "analyze:ST_CALL"}, "Excpected "+fn.params[pIndex]+" got string in function call", s.line)
}
case structureCode["L_BOOL"]:
if fn.params[pIndex] != "bool" {
return createError([]string{"analyze.go", "analyze:ST_CALL"}, "Excpected "+fn.params[pIndex]+" got bool in function call", s.line)
}
case structureCode["L_INT"]:
valid := false
types := []string{"int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64", "uintptr", "byte", "float32", "float64"}
for j := 0; j < len(types); j++ {
if types[j] == fn.params[pIndex] {
valid = true
break
}
}
if !valid {
return createError([]string{"analyze.go", "analyze:ST_CALL"}, "Excpected "+fn.params[pIndex]+" got int in function call", s.line)
}
default:
return createError([]string{"analyze.go", "analyze:ST_CALL"}, "How did you even...?", s.line)
}
i += 2
pIndex++
continue
}
The string
and bool
cases are quite simple. The default case shouldn't ever run because those are the only implemented types of literals. Lastly, when we find an int
we have to look through every int
type and see if there is any match. If there isn't, we throw an error.
Next
We are very close to the end of the month, so we are almost done Pogo. For the few days left I don't think classes would be feasible, however, we can try our best to implement them. It might be a bit crude however, since there is such a difference between structs and classes. But it's still worth a try.