Repository: d5/tengo (go)

Issue:
Function re-assignment in another function causes SIGSEGV in Tengo VM
The following Tengo code:

```golang
func1 := func(a, b) {
    return a * b
}

func2 := func() {
    test := test(1, 2)
}

func2()
```

While completely bonkers (re-assigning a function), produces a SIGSEGV, which should not happen.

```text
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x28 pc=0x52eb39]

goroutine 1 [running]:
github.com/d5/tengo/v2.(*VM).run(0xc000180000)
	/home/runner/work/tengo/tengo/vm.go:542 +0x2ac9
github.com/d5/tengo/v2.(*VM).Run(0xc000180000, 0xc000180000, 0x400)
	/home/runner/work/tengo/tengo/vm.go:77 +0xcf
main.CompileAndRun(0xc0001026f0, 0xc000152000, 0x5e, 0x25e, 0xc0001163d0, 0xc, 0x0, 0xc000104120)
	/home/runner/work/tengo/tengo/cmd/tengo/main.go:144 +0x1d0
main.main()
	/home/runner/work/tengo/tengo/cmd/tengo/main.go:83 +0x420
```

I stumbled upon this by happenstance as a result of a typo in my code.

Pull request:
fix local symbol resolution bug
Fixes #314.

This fixes a bug where an unassigned local variable is incorrectly referenced in 2 different scenarios:

```golang
(func() {
	fn := fn()
})()
```

This should fail compilation with `unresolved reference` error because `fn` is not yet defined.

```golang
fn := func() {}	
(func() {
    fn := fn()
    return fn
})()
```

This should compile and run correctly. `fn` oh the left-hand side is a new local variable and `fn` on the right-hand side is a global variable defined outside the function.

Patch:
diff --git a/compiler.go b/compiler.go
index 1937a1d..53cc7d3 100644
--- a/compiler.go
+++ b/compiler.go
@@ -323,7 +323,7 @@ func (c *Compiler) Compile(node parser.Node) error {
 			return err
 		}
 	case *parser.Ident:
-		symbol, _, ok := c.symbolTable.Resolve(node.Name)
+		symbol, _, ok := c.symbolTable.Resolve(node.Name, false)
 		if !ok {
 			return c.errorf(node, "unresolved reference '%s'", node.Name)
 		}
@@ -659,7 +659,7 @@ func (c *Compiler) compileAssign(
 		return c.errorf(node, "operator ':=' not allowed with selector")
 	}
 
-	symbol, depth, exists := c.symbolTable.Resolve(ident)
+	symbol, depth, exists := c.symbolTable.Resolve(ident, false)
 	if op == token.Define {
 		if depth == 0 && exists {
 			return c.errorf(node, "'%s' redeclared in this block", ident)
diff --git a/script.go b/script.go
index 4f9608c..46e4802 100644
--- a/script.go
+++ b/script.go
@@ -116,7 +116,7 @@ func (s *Script) Compile() (*Compiled, error) {
 	// global symbol names to indexes
 	globalIndexes := make(map[string]int, len(globals))
 	for _, name := range symbolTable.Names() {
-		symbol, _, _ := symbolTable.Resolve(name)
+		symbol, _, _ := symbolTable.Resolve(name, false)
 		if symbol.Scope == ScopeGlobal {
 			globalIndexes[name] = symbol.Index
 		}
diff --git a/symbol_table.go b/symbol_table.go
index 6ae5d7d..a1f25c7 100644
--- a/symbol_table.go
+++ b/symbol_table.go
@@ -71,25 +71,36 @@ func (t *SymbolTable) DefineBuiltin(index int, name string) *Symbol {
 // Resolve resolves a symbol with a given name.
 func (t *SymbolTable) Resolve(
 	name string,
-) (symbol *Symbol, depth int, ok bool) {
-	symbol, ok = t.store[name]
-	if !ok && t.parent != nil {
-		symbol, depth, ok = t.parent.Resolve(name)
-		if !ok {
-			return
+	recur bool,
+) (*Symbol, int, bool) {
+	symbol, ok := t.store[name]
+	if ok {
+		// symbol can be used if
+		if symbol.Scope != ScopeLocal || // it's not of local scope, OR,
+			symbol.LocalAssigned || // it's assigned at least once, OR,
+			recur { // it's defined in higher level
+			return symbol, 0, true
 		}
-		depth++
-
-		// if symbol is defined in parent table and if it's not global/builtin
-		// then it's free variable.
-		if !t.block && depth > 0 &&
-			symbol.Scope != ScopeGlobal &&
-			symbol.Scope != ScopeBuiltin {
-			return t.defineFree(symbol), depth, true
-		}
-		return
 	}
-	return
+
+	if t.parent == nil {
+		return nil, 0, false
+	}
+
+	symbol, depth, ok := t.parent.Resolve(name, true)
+	if !ok {
+		return nil, 0, false
+	}
+	depth++
+
+	// if symbol is defined in parent table and if it's not global/builtin
+	// then it's free variable.
+	if !t.block && depth > 0 &&
+		symbol.Scope != ScopeGlobal &&
+		symbol.Scope != ScopeBuiltin {
+		return t.defineFree(symbol), depth, true
+	}
+	return symbol, depth, true
 }
 
 // Fork creates a new symbol table for a new scope.


Test patch:
diff --git a/compiler_test.go b/compiler_test.go
index 905fa15..55801a4 100644
--- a/compiler_test.go
+++ b/compiler_test.go
@@ -1005,6 +1005,13 @@ r["x"] = {
 `, "Parse Error: illegal character U+0040 '@'\n\tat test:3:5 (and 10 more errors)")
 
 	expectCompileError(t, `import("")`, "empty module name")
+
+	// https://github.com/d5/tengo/issues/314
+	expectCompileError(t, `
+(func() {
+	fn := fn()
+})()	
+`, "unresolved reference 'fn")
 }
 
 func TestCompilerErrorReport(t *testing.T) {
diff --git a/symbol_table_test.go b/symbol_table_test.go
index 04833f4..e46eb3a 100644
--- a/symbol_table_test.go
+++ b/symbol_table_test.go
@@ -126,7 +126,7 @@ func resolveExpect(
 	expectedSymbol *tengo.Symbol,
 	expectedDepth int,
 ) {
-	actualSymbol, actualDepth, ok := symbolTable.Resolve(name)
+	actualSymbol, actualDepth, ok := symbolTable.Resolve(name, true)
 	require.True(t, ok)
 	require.Equal(t, expectedSymbol, actualSymbol)
 	require.Equal(t, expectedDepth, actualDepth)
diff --git a/vm_test.go b/vm_test.go
index c20fcd1..d5437a5 100644
--- a/vm_test.go
+++ b/vm_test.go
@@ -1792,6 +1792,16 @@ func() {
 		nil, true)
 	expectRun(t, `out = func(v) { for ;;v++ { if v == 3 { break } } }(1)`,
 		nil, tengo.UndefinedValue)
+
+	// 'f' in RHS at line 4 must reference global variable 'f'
+	// See https://github.com/d5/tengo/issues/314
+	expectRun(t, `
+f := func() { return 2 }
+out = (func() {
+	f := f()
+	return f
+})()
+	`, nil, 2)
 }
 
 func TestIf(t *testing.T) {
@@ -3753,7 +3763,7 @@ func traceCompileRun(
 	{
 		res = make(map[string]tengo.Object)
 		for name := range symbols {
-			sym, depth, ok := symTable.Resolve(name)
+			sym, depth, ok := symTable.Resolve(name, false)
 			if !ok || depth != 0 {
 				err = fmt.Errorf("symbol not found: %s", name)
 				return