Line Numbers in Errors (Pogo Pt:11)

Chig Beef - Jan 27 - - Dev Community

Intro

In this series I am creating a transpiler from Python to Golang called Pogo. In the last post we crudely implemented functions, which are very good at throwing errors. Since we are getting so many errors now it's a great time to fix them up and make them somewhat readable.

Where are we holding the line number?

The easiest way to hold the line number would be in the Tokens and Structures, since these are carried through all the stages of the compiler. We can just add a property called line as an int and give this number to the Token. But we need to get this number from somewhere, and for the Structures this isn't so much of an issue, just get it from the Token that is equivalent. This is done using p.curToken.line. But where does the Token get it from?

Getting the Line Number

Well the best place for this is the lexer. On the lexer we can also have a value keeping track of the current line, and every time we find a newline, we increment that value.

Testing

Here's a simple test case.

from GoType import *
1234.
Enter fullscreen mode Exit fullscreen mode

This should throw an error from the lexer, since we have to end a number with a digit. And we got our error, here it is.

[Lex (lex)] Numbers must end with a digit on line 2
Enter fullscreen mode Exit fullscreen mode

That's pretty good! The line number is very helpful here for the user, but what about the compiler designer? We definitely need to work on how useful those errors are.

Errors for the Compiler Designer

What would make errors better for us? Well it would be nice to know which function the error was created. This isn't that helpful though, since a lot of these errors would end up coming from checkToken. Instead, what we should ask is what function line this error came from, so we would get an error such as this.

parse.go -> parse -> program -> statement:ST_IMPORT
Expected ASTERISK, got R_PAREN on line 5
Enter fullscreen mode Exit fullscreen mode

That's a pretty good error, especially for such a simple transpiler like ours.

Keeping Track of the Function Line

To keep track of the function line we are going to have a slice of strings attached to the parser (we are only going to implement this in the parser, because every other file is way too small for this to matter, with the emitter being a single function).
Now, whenever we enter a function we need to add what function we are in to the slice of function names.

p.funcLine = append(p.funcLine, "program")
Enter fullscreen mode Exit fullscreen mode

Now we have to do that for every function.

Deleting Functions from the Function Line

Now we only need to delete the last item in the slice when we want to do this, so we use this.

p.funcLine = p.funcLine[:len(p.funcLine)-1]
Enter fullscreen mode Exit fullscreen mode

This will get rid of the last item. We just need to start placing these around our code. We want to put these at the end of every function, but most functions end at multiple places. However, we don't want to place this before we return an error, because then we erase the tracks we just made for this purpose. Instead, we want to place these when we don't return an error, when we return nil. This (usually) means we only place it once for every function.

Using What We Made

We've done all this work implementing the function line, but we haven't used it yet, we need to actually show these in the errors. To help simplify everything, we'll also make a function to help create errors.

func createError(funcLine []string, message string, line int) error {
    output := ""
    for i := 0; i < len(funcLine); i++ {
        output += funcLine[i]
        output += " -> "
    }
    output += "line: " + strconv.Itoa(line) + "\n"
    output += message
    return errors.New(output)
}
Enter fullscreen mode Exit fullscreen mode

This takes the work of creating errors out of the parser.

Checking the Error

Here is our input code.

from GoType import *
for 2 in range(2, 10):
    print(2)
Enter fullscreen mode Exit fullscreen mode

Our error here is that we used the number 2 instead of an identifier in line 2, right after for. Now we can run our compiler and see what error it gives us.

parse.go -> parse -> program -> statement -> checkTokenRange -> checkToken -> line: 2
Expected IDENTIFIER got 2
Enter fullscreen mode Exit fullscreen mode

Which is exactly what we want to see.

Next

I know I put it off for a bit but I should really work on that semantic analyzer and fix functions, we should be able to use functions just as we normally would without getting into errors.

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