langur

Jan. 19, 2020

This has been fixed in 0.6.14 and 0.5.10 (not in 0.6.0 to 0.6.13).

The fix is also described further down the page.

I recently discovered a bug in langur regarding wrapping things into scope. It appears it is even in version 1 of my book. It would affect the frame distance given for OpGetNonLocal, OpSetNonLocal, and OpSetNonLocalIndexedValue.

As an example, the following throws the VM into a panic. (It shouldn't.)

val .data = [[1, 2], [3, 4], [5, 6], [7, 8]] var .sum = 0 for .test in .data { var (.x, .y) = .test .sum += .x x .y catch writeln .err }

It works (or doesn't work) something like the following.

  1. The decoupling assignment from .test to the 2 variables on the left is wrapped into scope first. This means that .test is not local to the frame in which it is being retrieved, and will be retrieved with OpGetNonLocal. All seems well up to this point.

  2. The catch wraps the previous statements within a block into an implicit try block, and in doing so, creates another frame level. Then, the frame level given for a non-local is wrong. It was wrong, because the compiler used no symbol table for the try section, as it did not have scope.

  3. When the VM tries to access the locals slice at the expected frame level to retrieve the .test variable, it finds nothing, and throws an indexing error.

the fix

I fixed this by causing the compiler to use a "non-scope" symbol table to represent the fact that a section of code will be in a different frame in the VM, though it has no scope of its own.

  1. add an isNonScope field to symbol tables

    type SymbolTable struct { Outer *SymbolTable store map[string]symbol definitionCount int FreeSymbols []symbol isFunction bool isNonScope bool modes *modes.CompileModes }

  2. update SymbolTable.defineSymbol() to defer when found to not be a symbol table (non-scope)

    func (st *SymbolTable) defineSymbol(name string, mutable bool) (symbol, error) { if st.isNonScope { return st.Outer.defineSymbol(name, mutable) }

    This is safe, as there will always be an Outer symbol table (never set to nil) for a non-scope symbol table, since the first table is never a non-scope table.

  3. in compiler.go, add a little function to push a non-scope symbol table

    func (c *Compiler) pushNonScope() { c.symbolTable = NewSymbolTable(c.symbolTable, false, c.Modes) c.symbolTable.isNonScope = true }

  4. in flow.go, update compileTryCatch() to use a non-scope symbol table when compiling the try section

    func (c *Compiler) compileTryCatch(node *ast.TryCatchNode) (ins opcode.Instructions, err error) { var try, catch, tcelse opcode.Instructions // The try frame doesn't have scope, but catch and else frames do. c.pushNonScope() try, err = c.compileNode(node.Try, false) c.popVariableScope() if err != nil { return } tryIndex := c.addConstant(&object.CompiledCode{Instructions: try})

    We use c.popVariableScope() though it is "non-scope," as this will pop the current symbol table.