Calling Functions (Pogo Pt:13)

Chig Beef - Jan 29 - - Dev Community

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",
    })
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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 Functions to analyze.

func (a *Analyzer) analyze(s Structure, vars []Variable, funcs []Function) error
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .