Repository: benhoyt/goawk
Pull request #175 (2023-05-12 06:01:15)

Refactor and simplify resolver to use two passes
This rewrites the type resolver to hopefully be easier to understand and change. We'll see.

Instead of trying to collect all information in a single pass, it now uses two passes:

1) First pass determines function call graph order (topological order).
2) Second pass determines variable type information.

Also:

* Simplify/stabilize "goawk -dt" output and add test
* Remove Scope and Index info from AST (that's a resolver thing)
* Improve error positions for several type errors (hence the updated col:line values in test output)

**Update:** I think this is definitely an improvement, as https://github.com/benhoyt/goawk/pull/176 (`length(x)` working for arrays as well as strings) was *much* simpler to implement with the new version.



Patch:
diff --git a/internal/ast/ast.go b/internal/ast/ast.go
index 88d9ffc..98e1307 100644
--- a/internal/ast/ast.go
+++ b/internal/ast/ast.go
@@ -10,7 +10,7 @@ import (
 	. "github.com/benhoyt/goawk/lexer"
 )
 
-// Program is an entire AWK program.
+// Program is a parsed AWK program.
 type Program struct {
 	Begin     []Stmts
 	Actions   []*Action
@@ -18,14 +18,6 @@ type Program struct {
 	Functions []*Function
 }
 
-// ResolvedProgram is a parsed AWK program + additional data prepared by resolve step
-// needed for subsequent interpretation
-type ResolvedProgram struct {
-	Program
-	Scalars map[string]int
-	Arrays  map[string]int
-}
-
 // String returns an indented, pretty-printed version of the parsed
 // program.
 func (p *Program) String() string {
@@ -95,7 +87,6 @@ func (e *FieldExpr) node()      {}
 func (e *NamedFieldExpr) node() {}
 func (e *UnaryExpr) node()      {}
 func (e *BinaryExpr) node()     {}
-func (e *ArrayExpr) node()      {}
 func (e *InExpr) node()         {}
 func (e *CondExpr) node()       {}
 func (e *NumExpr) node()        {}
@@ -160,7 +151,6 @@ const (
 func (e *FieldExpr) precedence() int      { return precField }
 func (e *NamedFieldExpr) precedence() int { return precField }
 func (e *UnaryExpr) precedence() int      { return precUnary }
-func (e *ArrayExpr) precedence() int      { return precPrimary }
 func (e *InExpr) precedence() int         { return precIn }
 func (e *CondExpr) precedence() int       { return precCond }
 func (e *NumExpr) precedence() int        { return precPrimary }
@@ -260,35 +250,22 @@ func (e *BinaryExpr) String() string {
 	return parenthesize(e.Left, e) + op + parenthesize(e.Right, e)
 }
 
-// ArrayExpr is an array reference. Not really a stand-alone
-// expression, except as an argument to split() or a user function
-// call.
-type ArrayExpr struct {
-	Scope VarScope
-	Index int
-	Name  string
-	Pos   Position
-}
-
-func (e *ArrayExpr) String() string {
-	return e.Name
-}
-
 // InExpr is an expression like (index in array).
 type InExpr struct {
-	Index []Expr
-	Array *ArrayExpr
+	Index    []Expr
+	Array    string
+	ArrayPos Position
 }
 
 func (e *InExpr) String() string {
 	if len(e.Index) == 1 {
-		return parenthesize(e.Index[0], e) + " in " + e.Array.String()
+		return parenthesize(e.Index[0], e) + " in " + e.Array
 	}
 	indices := make([]string, len(e.Index))
 	for i, index := range e.Index {
 		indices[i] = index.String()
 	}
-	return "(" + strings.Join(indices, ", ") + ") in " + e.Array.String()
+	return "(" + strings.Join(indices, ", ") + ") in " + e.Array
 }
 
 // CondExpr is an expression like cond ? 1 : 0.
@@ -335,25 +312,10 @@ func (e *RegExpr) String() string {
 	return "/" + escaped + "/"
 }
 
-// meaning it will be set during resolve step
-const resolvedLater = -1
-
-type VarScope int
-
-const (
-	ScopeSpecial VarScope = iota
-	ScopeGlobal
-	ScopeLocal
-)
-
 // VarExpr is a variable reference (special var, global, or local).
-// Index is the resolved variable index used by the interpreter; Name
-// is the original name used by String().
 type VarExpr struct {
-	Scope VarScope
-	Index int
-	Name  string
-	Pos   Position
+	Name string
+	Pos  Position
 }
 
 func (e *VarExpr) String() string {
@@ -362,8 +324,9 @@ func (e *VarExpr) String() string {
 
 // IndexExpr is an expression like a[k] (rvalue or lvalue).
 type IndexExpr struct {
-	Array *ArrayExpr
-	Index []Expr
+	Array    string
+	ArrayPos Position
+	Index    []Expr
 }
 
 func (e *IndexExpr) String() string {
@@ -371,7 +334,7 @@ func (e *IndexExpr) String() string {
 	for i, index := range e.Index {
 		indices[i] = index.String()
 	}
-	return e.Array.String() + "[" + strings.Join(indices, ", ") + "]"
+	return e.Array + "[" + strings.Join(indices, ", ") + "]"
 }
 
 // AssignExpr is an expression like x = 1234.
@@ -424,16 +387,12 @@ func (e *CallExpr) String() string {
 	return e.Func.String() + "(" + strings.Join(args, ", ") + ")"
 }
 
-// UserCallExpr is a user-defined function call like my_func(1, 2, 3)
-//
-// Index is the resolved function index used by the interpreter; Name
-// is the original name used by String().
+// UserCallExpr is a user-defined function call like my_func(1, 2, 3),
+// where my_func is either AWK-defined or a native Go function.
 type UserCallExpr struct {
-	Native bool // false = AWK-defined function, true = native Go func
-	Index  int
-	Name   string
-	Args   []Expr
-	Pos    Position
+	Name string
+	Args []Expr
+	Pos  Position
 }
 
 func (e *UserCallExpr) String() string {
@@ -622,7 +581,7 @@ type IfStmt struct {
 }
 
 func (s *IfStmt) String() string {
-	str := "if (" + trimParens(s.Cond.String()) + ") {\n" + s.Body.String() + "}"
+	str := "if (" + s.Cond.String() + ") {\n" + s.Body.String() + "}"
 	if len(s.Else) > 0 {
 		str += " else {\n" + s.Else.String() + "}"
 	}
@@ -647,7 +606,7 @@ func (s *ForStmt) String() string {
 	}
 	condStr := ""
 	if s.Cond != nil {
-		condStr = " " + trimParens(s.Cond.String())
+		condStr = " " + s.Cond.String()
 	}
 	postStr := ""
 	if s.Post != nil {
@@ -658,8 +617,10 @@ func (s *ForStmt) String() string {
 
 // ForInStmt is a for loop like for (k in a) print k, a[k].
 type ForInStmt struct {
-	Var       *VarExpr
-	Array     *ArrayExpr
+	Var       string
+	VarPos    Position
+	Array     string
+	ArrayPos  Position
 	BodyStart Position
 	Body      Stmts
 	Start     Position
@@ -667,7 +628,7 @@ type ForInStmt struct {
 }
 
 func (s *ForInStmt) String() string {
-	return "for (" + s.Var.String() + " in " + s.Array.String() + ") {\n" + s.Body.String() + "}"
+	return "for (" + s.Var + " in " + s.Array + ") {\n" + s.Body.String() + "}"
 }
 
 // WhileStmt is a while loop.
@@ -680,7 +641,7 @@ type WhileStmt struct {
 }
 
 func (s *WhileStmt) String() string {
-	return "while (" + trimParens(s.Cond.String()) + ") {\n" + s.Body.String() + "}"
+	return "while (" + s.Cond.String() + ") {\n" + s.Body.String() + "}"
 }
 
 // DoWhileStmt is a do-while loop.
@@ -692,7 +653,7 @@ type DoWhileStmt struct {
 }
 
 func (s *DoWhileStmt) String() string {
-	return "do {\n" + s.Body.String() + "} while (" + trimParens(s.Cond.String()) + ")"
+	return "do {\n" + s.Body.String() + "} while (" + s.Cond.String() + ")"
 }
 
 // BreakStmt is a break statement.
@@ -752,18 +713,22 @@ func (s *ExitStmt) String() string {
 
 // DeleteStmt is a statement like delete a[k].
 type DeleteStmt struct {
-	Array *ArrayExpr
-	Index []Expr
-	Start Position
-	End   Position
+	Array    string
+	ArrayPos Position
+	Index    []Expr
+	Start    Position
+	End      Position
 }
 
 func (s *DeleteStmt) String() string {
+	if len(s.Index) == 0 {
+		return "delete " + s.Array
+	}
 	indices := make([]string, len(s.Index))
 	for i, index := range s.Index {
 		indices[i] = index.String()
 	}
-	return "delete " + s.Array.String() + "[" + strings.Join(indices, ", ") + "]"
+	return "delete " + s.Array + "[" + strings.Join(indices, ", ") + "]"
 }
 
 // ReturnStmt is a return statement.
@@ -796,7 +761,6 @@ func (s *BlockStmt) String() string {
 type Function struct {
 	Name   string
 	Params []string
-	Arrays []bool
 	Body   Stmts
 	Pos    Position
 }
@@ -806,28 +770,6 @@ func (f *Function) String() string {
 		f.Body.String() + "}"
 }
 
-func trimParens(s string) string {
-	if strings.HasPrefix(s, "(") && strings.HasSuffix(s, ")") {
-		s = s[1 : len(s)-1]
-	}
-	return s
-}
-
-// VarRef is a constructor for *VarExpr
-func VarRef(name string, pos Position) *VarExpr {
-	return &VarExpr{resolvedLater, resolvedLater, name, pos}
-}
-
-// ArrayRef is a constructor for *ArrayExpr
-func ArrayRef(name string, pos Position) *ArrayExpr {
-	return &ArrayExpr{resolvedLater, resolvedLater, name, pos}
-}
-
-// UserCall is a constructor for *UserCallExpr
-func UserCall(name string, args []Expr, pos Position) *UserCallExpr {
-	return &UserCallExpr{false, resolvedLater, name, args, pos}
-}
-
 // PositionError represents an error bound to specific position in source.
 type PositionError struct {
 	// Source line/column position where the error occurred.
diff --git a/internal/ast/walk.go b/internal/ast/walk.go
index 96a2f75..5927d5f 100644
--- a/internal/ast/walk.go
+++ b/internal/ast/walk.go
@@ -2,7 +2,7 @@ package ast
 
 import "fmt"
 
-// A Visitor's Visit method is invoked for each node encountered by Walk.
+// Visitor has a Visit method which is invoked for each node encountered by Walk.
 // If the result visitor w is not nil, Walk visits each of the children
 // of node with the visitor w, followed by a call of w.Visit(nil).
 type Visitor interface {
@@ -55,10 +55,8 @@ func Walk(v Visitor, node Node) {
 		Walk(v, n.Left)
 		Walk(v, n.Right)
 
-	case *ArrayExpr: // leaf
 	case *InExpr:
 		WalkExprList(v, n.Index)
-		Walk(v, n.Array)
 
 	case *CondExpr:
 		Walk(v, n.Cond)
@@ -70,7 +68,6 @@ func Walk(v Visitor, node Node) {
 	case *RegExpr: // leaf
 	case *VarExpr: // leaf
 	case *IndexExpr:
-		Walk(v, n.Array)
 		WalkExprList(v, n.Index)
 
 	case *AssignExpr:
@@ -125,8 +122,6 @@ func Walk(v Visitor, node Node) {
 		WalkStmtList(v, n.Body)
 
 	case *ForInStmt:
-		Walk(v, n.Var)
-		Walk(v, n.Array)
 		WalkStmtList(v, n.Body)
 
 	case *WhileStmt:
@@ -145,7 +140,6 @@ func Walk(v Visitor, node Node) {
 		Walk(v, n.Status)
 
 	case *DeleteStmt:
-		Walk(v, n.Array)
 		WalkExprList(v, n.Index)
 
 	case *ReturnStmt:
diff --git a/internal/compiler/compiler.go b/internal/compiler/compiler.go
index 71285bb..9f888e0 100644
--- a/internal/compiler/compiler.go
+++ b/internal/compiler/compiler.go
@@ -8,6 +8,7 @@ import (
 	"strconv"
 
 	"github.com/benhoyt/goawk/internal/ast"
+	"github.com/benhoyt/goawk/internal/resolver"
 	"github.com/benhoyt/goawk/lexer"
 )
 
@@ -55,7 +56,7 @@ func (e *compileError) Error() string {
 }
 
 // Compile compiles an AST (parsed program) into virtual machine instructions.
-func Compile(prog *ast.ResolvedProgram) (compiledProg *Program, err error) {
+func Compile(resolved *resolver.ResolvedProgram) (compiledProg *Program, err error) {
 	defer func() {
 		// The compiler uses panic with a *compileError to signal compile
 		// errors internally, and they're caught here. This avoids the
@@ -78,57 +79,60 @@ func Compile(prog *ast.ResolvedProgram) (compiledProg *Program, err error) {
 	// Compile functions. For functions called before they're defined or
 	// recursive functions, we have to set most p.Functions data first, then
 	// compile Body afterward.
-	p.Functions = make([]Function, len(prog.Functions))
-	for i, astFunc := range prog.Functions {
+	p.Functions = make([]Function, len(resolved.Functions))
+	for i, astFunc := range resolved.Functions {
+		arrays := make([]bool, len(astFunc.Params))
 		numArrays := 0
-		for _, a := range astFunc.Arrays {
-			if a {
+		for j, param := range astFunc.Params {
+			_, info, _ := resolved.LookupVar(astFunc.Name, param)
+			if info.Type == resolver.Array {
+				arrays[j] = true
 				numArrays++
 			}
 		}
 		compiledFunc := Function{
 			Name:       astFunc.Name,
 			Params:     astFunc.Params,
-			Arrays:     astFunc.Arrays,
-			NumScalars: len(astFunc.Arrays) - numArrays,
+			Arrays:     arrays,
+			NumScalars: len(astFunc.Params) - numArrays,
 			NumArrays:  numArrays,
 		}
 		p.Functions[i] = compiledFunc
 	}
-	for i, astFunc := range prog.Functions {
-		c := &compiler{program: p, indexes: indexes}
+	for i, astFunc := range resolved.Functions {
+		c := compiler{resolved: resolved, program: p, indexes: indexes, funcName: astFunc.Name}
 		c.stmts(astFunc.Body)
 		p.Functions[i].Body = c.finish()
 	}
 
 	// Compile BEGIN blocks.
-	for _, stmts := range prog.Begin {
-		c := &compiler{program: p, indexes: indexes}
+	for _, stmts := range resolved.Begin {
+		c := compiler{resolved: resolved, program: p, indexes: indexes}
 		c.stmts(stmts)
 		p.Begin = append(p.Begin, c.finish()...)
 	}
 
 	// Compile pattern-action blocks.
-	for _, action := range prog.Actions {
+	for _, action := range resolved.Actions {
 		var pattern [][]Opcode
 		switch len(action.Pattern) {
 		case 0:
 			// Always considered a match
 		case 1:
-			c := &compiler{program: p, indexes: indexes}
+			c := compiler{resolved: resolved, program: p, indexes: indexes}
 			c.expr(action.Pattern[0])
 			pattern = [][]Opcode{c.finish()}
 		case 2:
-			c := &compiler{program: p, indexes: indexes}
+			c := compiler{resolved: resolved, program: p, indexes: indexes}
 			c.expr(action.Pattern[0])
 			pattern = append(pattern, c.finish())
-			c = &compiler{program: p, indexes: indexes}
+			c = compiler{resolved: resolved, program: p, indexes: indexes}
 			c.expr(action.Pattern[1])
 			pattern = append(pattern, c.finish())
 		}
 		var body []Opcode
 		if len(action.Stmts) > 0 {
-			c := &compiler{program: p, indexes: indexes}
+			c := compiler{resolved: resolved, program: p, indexes: indexes}
 			c.stmts(action.Stmts)
 			body = c.finish()
 		}
@@ -139,21 +143,33 @@ func Compile(prog *ast.ResolvedProgram) (compiledProg *Program, err error) {
 	}
 
 	// Compile END blocks.
-	for _, stmts := range prog.End {
-		c := &compiler{program: p, indexes: indexes}
+	for _, stmts := range resolved.End {
+		c := compiler{resolved: resolved, program: p, indexes: indexes}
 		c.stmts(stmts)
 		p.End = append(p.End, c.finish()...)
 	}
 
+	// Build slices that map indexes to names (for variables and functions).
 	// These are only used for disassembly, but set them up here.
-	p.scalarNames = make([]string, len(prog.Scalars))
-	for name, index := range prog.Scalars {
-		p.scalarNames[index] = name
-	}
-	p.arrayNames = make([]string, len(prog.Arrays))
-	for name, index := range prog.Arrays {
-		p.arrayNames[index] = name
-	}
+	resolved.IterVars("", func(name string, info resolver.VarInfo) {
+		if info.Type == resolver.Array {
+			for len(p.arrayNames) <= info.Index {
+				p.arrayNames = append(p.arrayNames, "")
+			}
+			p.arrayNames[info.Index] = name
+		} else {
+			for len(p.scalarNames) <= info.Index {
+				p.scalarNames = append(p.scalarNames, "")
+			}
+			p.scalarNames[info.Index] = name
+		}
+	})
+	resolved.IterFuncs(func(name string, info resolver.FuncInfo) {
+		for len(p.nativeFuncNames) <= info.Index {
+			p.nativeFuncNames = append(p.nativeFuncNames, "")
+		}
+		p.nativeFuncNames[info.Index] = name
+	})
 
 	return p, nil
 }
@@ -167,13 +183,31 @@ type constantIndexes struct {
 
 // Holds the compilation state.
 type compiler struct {
+	resolved  *resolver.ResolvedProgram
 	program   *Program
 	indexes   constantIndexes
+	funcName  string
 	code      []Opcode
 	breaks    [][]int
 	continues [][]int
 }
 
+func (c *compiler) scalarInfo(name string) (scope resolver.Scope, index int) {
+	scope, info, _ := c.resolved.LookupVar(c.funcName, name)
+	if info.Type != resolver.Scalar {
+		panic(fmt.Sprintf("internal error: found %s when expecting scalar %q", info.Type, name))
+	}
+	return scope, info.Index
+}
+
+func (c *compiler) arrayInfo(name string) (scope resolver.Scope, index int) {
+	scope, info, _ := c.resolved.LookupVar(c.funcName, name)
+	if info.Type != resolver.Array {
+		panic(fmt.Sprintf("internal error: found %s when expecting array %q", info.Type, name))
+	}
+	return scope, info.Index
+}
+
 func (c *compiler) add(ops ...Opcode) {
 	c.code = append(c.code, ops...)
 }
@@ -202,24 +236,26 @@ func (c *compiler) stmt(stmt ast.Stmt) {
 			// Pre or post doesn't matter for an assignment expression
 			switch target := expr.Expr.(type) {
 			case *ast.VarExpr:
-				switch target.Scope {
-				case ast.ScopeGlobal:
-					c.add(IncrGlobal, incrAmount(expr.Op), opcodeInt(target.Index))
-				case ast.ScopeLocal:
-					c.add(IncrLocal, incrAmount(expr.Op), opcodeInt(target.Index))
+				scope, index := c.scalarInfo(target.Name)
+				switch scope {
+				case resolver.Global:
+					c.add(IncrGlobal, incrAmount(expr.Op), opcodeInt(index))
+				case resolver.Local:
+					c.add(IncrLocal, incrAmount(expr.Op), opcodeInt(index))
 				default: // ScopeSpecial
-					c.add(IncrSpecial, incrAmount(expr.Op), opcodeInt(target.Index))
+					c.add(IncrSpecial, incrAmount(expr.Op), opcodeInt(index))
 				}
 			case *ast.FieldExpr:
 				c.expr(target.Index)
 				c.add(IncrField, incrAmount(expr.Op))
 			case *ast.IndexExpr:
 				c.index(target.Index)
-				switch target.Array.Scope {
-				case ast.ScopeGlobal:
-					c.add(IncrArrayGlobal, incrAmount(expr.Op), opcodeInt(target.Array.Index))
+				scope, index := c.arrayInfo(target.Array)
+				switch scope {
+				case resolver.Global:
+					c.add(IncrArrayGlobal, incrAmount(expr.Op), opcodeInt(index))
 				default: // ScopeLocal
-					c.add(IncrArrayLocal, incrAmount(expr.Op), opcodeInt(target.Array.Index))
+					c.add(IncrArrayLocal, incrAmount(expr.Op), opcodeInt(index))
 				}
 			}
 			return
@@ -245,24 +281,26 @@ func (c *compiler) stmt(stmt ast.Stmt) {
 
 			switch target := expr.Left.(type) {
 			case *ast.VarExpr:
-				switch target.Scope {
-				case ast.ScopeGlobal:
-					c.add(AugAssignGlobal, Opcode(augOp), opcodeInt(target.Index))
-				case ast.ScopeLocal:
-					c.add(AugAssignLocal, Opcode(augOp), opcodeInt(target.Index))
+				scope, index := c.scalarInfo(target.Name)
+				switch scope {
+				case resolver.Global:
+					c.add(AugAssignGlobal, Opcode(augOp), opcodeInt(index))
+				case resolver.Local:
+					c.add(AugAssignLocal, Opcode(augOp), opcodeInt(index))
 				default: // ScopeSpecial
-					c.add(AugAssignSpecial, Opcode(augOp), opcodeInt(target.Index))
+					c.add(AugAssignSpecial, Opcode(augOp), opcodeInt(index))
 				}
 			case *ast.FieldExpr:
 				c.expr(target.Index)
 				c.add(AugAssignField, Opcode(augOp))
 			case *ast.IndexExpr:
 				c.index(target.Index)
-				switch target.Array.Scope {
-				case ast.ScopeGlobal:
-					c.add(AugAssignArrayGlobal, Opcode(augOp), opcodeInt(target.Array.Index))
+				scope, index := c.arrayInfo(target.Array)
+				switch scope {
+				case resolver.Global:
+					c.add(AugAssignArrayGlobal, Opcode(augOp), opcodeInt(index))
 				default: // ScopeLocal
-					c.add(AugAssignArrayLocal, Opcode(augOp), opcodeInt(target.Array.Index))
+					c.add(AugAssignArrayLocal, Opcode(augOp), opcodeInt(index))
 				}
 			}
 			return
@@ -346,8 +384,10 @@ func (c *compiler) stmt(stmt ast.Stmt) {
 		// Otherwise we'd need to build a slice of all keys rather than
 		// iterating, or write our own hash table that has a more flexible
 		// iterator.
-		mark := c.jumpForward(ForIn, opcodeInt(int(s.Var.Scope)), opcodeInt(s.Var.Index),
-			Opcode(s.Array.Scope), opcodeInt(s.Array.Index))
+		varScope, varIndex := c.scalarInfo(s.Var)
+		arrayScope, arrayIndex := c.arrayInfo(s.Array)
+		mark := c.jumpForward(ForIn, opcodeInt(int(varScope)), opcodeInt(varIndex),
+			Opcode(arrayScope), opcodeInt(arrayIndex))
 
 		c.breaks = append(c.breaks, nil) // nil tells BreakStmt it's a for-in loop
 		c.continues = append(c.continues, []int{})
@@ -428,11 +468,12 @@ func (c *compiler) stmt(stmt ast.Stmt) {
 		c.add(Exit)
 
 	case *ast.DeleteStmt:
+		scope, index := c.arrayInfo(s.Array)
 		if len(s.Index) > 0 {
 			c.index(s.Index)
-			c.add(Delete, Opcode(s.Array.Scope), opcodeInt(s.Array.Index))
+			c.add(Delete, Opcode(scope), opcodeInt(index))
 		} else {
-			c.add(DeleteAll, Opcode(s.Array.Scope), opcodeInt(s.Array.Index))
+			c.add(DeleteAll, Opcode(scope), opcodeInt(index))
 		}
 
 	case *ast.BlockStmt:
@@ -457,24 +498,26 @@ func incrAmount(op lexer.Token) Opcode {
 func (c *compiler) assign(target ast.Expr) {
 	switch target := target.(type) {
 	case *ast.VarExpr:
-		switch target.Scope {
-		case ast.ScopeGlobal:
-			c.add(AssignGlobal, opcodeInt(target.Index))
-		case ast.ScopeLocal:
-			c.add(AssignLocal, opcodeInt(target.Index))
-		case ast.ScopeSpecial:
-			c.add(AssignSpecial, opcodeInt(target.Index))
+		scope, index := c.scalarInfo(target.Name)
+		switch scope {
+		case resolver.Global:
+			c.add(AssignGlobal, opcodeInt(index))
+		case resolver.Local:
+			c.add(AssignLocal, opcodeInt(index))
+		case resolver.Special:
+			c.add(AssignSpecial, opcodeInt(index))
 		}
 	case *ast.FieldExpr:
 		c.expr(target.Index)
 		c.add(AssignField)
 	case *ast.IndexExpr:
 		c.index(target.Index)
-		switch target.Array.Scope {
-		case ast.ScopeGlobal:
-			c.add(AssignArrayGlobal, opcodeInt(target.Array.Index))
-		case ast.ScopeLocal:
-			c.add(AssignArrayLocal, opcodeInt(target.Array.Index))
+		scope, index := c.arrayInfo(target.Array)
+		switch scope {
+		case resolver.Global:
+			c.add(AssignArrayGlobal, opcodeInt(index))
+		case resolver.Local:
+			c.add(AssignArrayLocal, opcodeInt(index))
 		}
 	}
 }
@@ -615,13 +658,14 @@ func (c *compiler) expr(expr ast.Expr) {
 		c.add(FieldByName)
 
 	case *ast.VarExpr:
-		switch e.Scope {
-		case ast.ScopeGlobal:
-			c.add(Global, opcodeInt(e.Index))
-		case ast.ScopeLocal:
-			c.add(Local, opcodeInt(e.Index))
-		case ast.ScopeSpecial:
-			c.add(Special, opcodeInt(e.Index))
+		scope, index := c.scalarInfo(e.Name)
+		switch scope {
+		case resolver.Global:
+			c.add(Global, opcodeInt(index))
+		case resolver.Local:
+			c.add(Local, opcodeInt(index))
+		case resolver.Special:
+			c.add(Special, opcodeInt(index))
 		}
 
 	case *ast.RegExpr:
@@ -702,11 +746,12 @@ func (c *compiler) expr(expr ast.Expr) {
 
 	case *ast.IndexExpr:
 		c.index(e.Index)
-		switch e.Array.Scope {
-		case ast.ScopeGlobal:
-			c.add(ArrayGlobal, opcodeInt(e.Array.Index))
-		case ast.ScopeLocal:
-			c.add(ArrayLocal, opcodeInt(e.Array.Index))
+		scope, index := c.arrayInfo(e.Array)
+		switch scope {
+		case resolver.Global:
+			c.add(ArrayGlobal, opcodeInt(index))
+		case resolver.Local:
+			c.add(ArrayLocal, opcodeInt(index))
 		}
 
 	case *ast.CallExpr:
@@ -714,12 +759,13 @@ func (c *compiler) expr(expr ast.Expr) {
 		switch e.Func {
 		case lexer.F_SPLIT:
 			c.expr(e.Args[0])
-			arrayExpr := e.Args[1].(*ast.ArrayExpr)
+			varExpr := e.Args[1].(*ast.VarExpr) // split()'s 2nd arg is always an array
+			scope, index := c.arrayInfo(varExpr.Name)
 			if len(e.Args) > 2 {
 				c.expr(e.Args[2])
-				c.add(CallSplitSep, Opcode(arrayExpr.Scope), opcodeInt(arrayExpr.Index))
+				c.add(CallSplitSep, Opcode(scope), opcodeInt(index))
 			} else {
-				c.add(CallSplit, Opcode(arrayExpr.Scope), opcodeInt(arrayExpr.Index))
+				c.add(CallSplit, Opcode(scope), opcodeInt(index))
 			}
 			return
 		case lexer.F_SUB, lexer.F_GSUB:
@@ -814,31 +860,30 @@ func (c *compiler) expr(expr ast.Expr) {
 
 	case *ast.InExpr:
 		c.index(e.Index)
-		switch e.Array.Scope {
-		case ast.ScopeGlobal:
-			c.add(InGlobal, opcodeInt(e.Array.Index))
+		scope, index := c.arrayInfo(e.Array)
+		switch scope {
+		case resolver.Global:
+			c.add(InGlobal, opcodeInt(index))
 		default: // ScopeLocal
-			c.add(InLocal, opcodeInt(e.Array.Index))
+			c.add(InLocal, opcodeInt(index))
 		}
 
 	case *ast.UserCallExpr:
-		if e.Native {
+		funcInfo, _ := c.resolved.LookupFunc(e.Name)
+		if funcInfo.Native {
 			for _, arg := range e.Args {
 				c.expr(arg)
 			}
-			c.add(CallNative, opcodeInt(e.Index), opcodeInt(len(e.Args)))
-			for len(c.program.nativeFuncNames) <= e.Index {
-				c.program.nativeFuncNames = append(c.program.nativeFuncNames, "")
-			}
-			c.program.nativeFuncNames[e.Index] = e.Name
+			c.add(CallNative, opcodeInt(funcInfo.Index), opcodeInt(len(e.Args)))
 		} else {
-			f := c.program.Functions[e.Index]
+			f := c.program.Functions[funcInfo.Index]
 			var arrayOpcodes []Opcode
 			numScalarArgs := 0
 			for i, arg := range e.Args {
 				if f.Arrays[i] {
 					a := arg.(*ast.VarExpr)
-					arrayOpcodes = append(arrayOpcodes, Opcode(a.Scope), opcodeInt(a.Index))
+					scope, index := c.arrayInfo(a.Name)
+					arrayOpcodes = append(arrayOpcodes, Opcode(scope), opcodeInt(index))
 				} else {
 					c.expr(arg)
 					numScalarArgs++
@@ -847,7 +892,7 @@ func (c *compiler) expr(expr ast.Expr) {
 			if numScalarArgs < f.NumScalars {
 				c.add(Nulls, opcodeInt(f.NumScalars-numScalarArgs))
 			}
-			c.add(CallUser, opcodeInt(e.Index), opcodeInt(len(arrayOpcodes)/2))
+			c.add(CallUser, opcodeInt(funcInfo.Index), opcodeInt(len(arrayOpcodes)/2))
 			c.add(arrayOpcodes...)
 		}
 
@@ -866,20 +911,22 @@ func (c *compiler) expr(expr ast.Expr) {
 		}
 		switch target := e.Target.(type) {
 		case *ast.VarExpr:
-			switch target.Scope {
-			case ast.ScopeGlobal:
-				c.add(GetlineGlobal, redirect(), opcodeInt(target.Index))
-			case ast.ScopeLocal:
-				c.add(GetlineLocal, redirect(), opcodeInt(target.Index))
-			case ast.ScopeSpecial:
-				c.add(GetlineSpecial, redirect(), opcodeInt(target.Index))
+			scope, index := c.scalarInfo(target.Name)
+			switch scope {
+			case resolver.Global:
+				c.add(GetlineGlobal, redirect(), opcodeInt(index))
+			case resolver.Local:
+				c.add(GetlineLocal, redirect(), opcodeInt(index))
+			case resolver.Special:
+				c.add(GetlineSpecial, redirect(), opcodeInt(index))
 			}
 		case *ast.FieldExpr:
 			c.expr(target.Index)
 			c.add(GetlineField, redirect())
 		case *ast.IndexExpr:
 			c.index(target.Index)
-			c.add(GetlineArray, redirect(), Opcode(target.Array.Scope), opcodeInt(target.Array.Index))
+			scope, index := c.arrayInfo(target.Array)
+			c.add(GetlineArray, redirect(), Opcode(scope), opcodeInt(index))
 		default:
 			c.add(Getline, redirect())
 		}
diff --git a/internal/compiler/disassembler.go b/internal/compiler/disassembler.go
index d5dc959..923244c 100644
--- a/internal/compiler/disassembler.go
+++ b/internal/compiler/disassembler.go
@@ -8,6 +8,7 @@ import (
 	"strings"
 
 	"github.com/benhoyt/goawk/internal/ast"
+	"github.com/benhoyt/goawk/internal/resolver"
 	"github.com/benhoyt/goawk/lexer"
 )
 
@@ -200,12 +201,12 @@ func (d *disassembler) disassemble(prefix string) error {
 			d.writeOpf("AssignArrayLocal %s", d.localArrayName(arrayIndex))
 
 		case Delete:
-			arrayScope := ast.VarScope(d.fetch())
+			arrayScope := resolver.Scope(d.fetch())
 			arrayIndex := int(d.fetch())
 			d.writeOpf("Delete %s", d.arrayName(arrayScope, arrayIndex))
 
 		case DeleteAll:
-			arrayScope := ast.VarScope(d.fetch())
+			arrayScope := resolver.Scope(d.fetch())
 			arrayIndex := int(d.fetch())
 			d.writeOpf("DeleteAll %s", d.arrayName(arrayScope, arrayIndex))
 
@@ -316,9 +317,9 @@ func (d *disassembler) disassemble(prefix string) error {
 			d.writeOpf("JumpGreaterOrEqual 0x%04x", d.ip+int(offset))
 
 		case ForIn:
-			varScope := ast.VarScope(d.fetch())
+			varScope := resolver.Scope(d.fetch())
 			varIndex := int(d.fetch())
-			arrayScope := ast.VarScope(d.fetch())
+			arrayScope := resolver.Scope(d.fetch())
 			arrayIndex := int(d.fetch())
 			offset := d.fetch()
 			d.writeOpf("ForIn %s %s 0x%04x", d.varName(varScope, varIndex), d.arrayName(arrayScope, arrayIndex), d.ip+int(offset))
@@ -328,12 +329,12 @@ func (d *disassembler) disassemble(prefix string) error {
 			d.writeOpf("CallBuiltin %s", builtinOp)
 
 		case CallSplit:
-			arrayScope := ast.VarScope(d.fetch())
+			arrayScope := resolver.Scope(d.fetch())
 			arrayIndex := int(d.fetch())
 			d.writeOpf("CallSplit %s", d.arrayName(arrayScope, arrayIndex))
 
 		case CallSplitSep:
-			arrayScope := ast.VarScope(d.fetch())
+			arrayScope := resolver.Scope(d.fetch())
 			arrayIndex := int(d.fetch())
 			d.writeOpf("CallSplitSep %s", d.arrayName(arrayScope, arrayIndex))
 
@@ -346,7 +347,7 @@ func (d *disassembler) disassemble(prefix string) error {
 			numArrayArgs := int(d.fetch())
 			var arrayArgs []string
 			for i := 0; i < numArrayArgs; i++ {
-				arrayScope := ast.VarScope(d.fetch())
+				arrayScope := resolver.Scope(d.fetch())
 				arrayIndex := int(d.fetch())
 				arrayArgs = append(arrayArgs, d.arrayName(arrayScope, arrayIndex))
 			}
@@ -404,7 +405,7 @@ func (d *disassembler) disassemble(prefix string) error {
 
 		case GetlineArray:
 			redirect := lexer.Token(d.fetch())
-			arrayScope := ast.VarScope(d.fetch())
+			arrayScope := resolver.Scope(d.fetch())
 			arrayIndex := int(d.fetch())
 			d.writeOpf("GetlineArray %s %s", redirect, d.arrayName(arrayScope, arrayIndex))
 
@@ -443,13 +444,13 @@ func (d *disassembler) writeOpf(format string, args ...interface{}) {
 }
 
 // Return the scalar variable name described by scope and index.
-func (d *disassembler) varName(scope ast.VarScope, index int) string {
+func (d *disassembler) varName(scope resolver.Scope, index int) string {
 	switch scope {
-	case ast.ScopeGlobal:
+	case resolver.Global:
 		return d.program.scalarNames[index]
-	case ast.ScopeLocal:
+	case resolver.Local:
 		return d.localName(index)
-	default: // ScopeSpecial
+	default: // resolver.Special
 		return ast.SpecialVarName(index)
 	}
 }
@@ -471,8 +472,8 @@ func (d *disassembler) localName(index int) string {
 }
 
 // Return the array variable name describes by scope and index.
-func (d *disassembler) arrayName(scope ast.VarScope, index int) string {
-	if scope == ast.ScopeLocal {
+func (d *disassembler) arrayName(scope resolver.Scope, index int) string {
+	if scope == resolver.Local {
 		return d.localArrayName(index)
 	}
 	return d.program.arrayNames[index]
diff --git a/internal/cover/cover.go b/internal/cover/cover.go
index 2834d3c..cab0827 100644
--- a/internal/cover/cover.go
+++ b/internal/cover/cover.go
@@ -242,7 +242,7 @@ func (cover *Cover) trackStatement(stmts []ast.Stmt) ast.Stmt {
 		numStmts: len(stmts),
 	})
 	left := &ast.IndexExpr{
-		Array: ast.ArrayRef(ArrayName, lexer.Position{}),
+		Array: ArrayName,
 		Index: []ast.Expr{&ast.NumExpr{Value: float64(len(cover.trackedBlocks))}},
 	}
 	if cover.mode == ModeCount {
diff --git a/internal/resolver/resolve.go b/internal/resolver/resolve.go
index 155bd53..8c1f9d3 100644
--- a/internal/resolver/resolve.go
+++ b/internal/resolver/resolve.go
@@ -1,4 +1,5 @@
-// Resolve function calls and variable types
+// Package resolver assigns integer indexes to functions and variables, as
+// well as determining and checking their types (scalar or array).
 package resolver
 
 import (
@@ -6,504 +7,476 @@ import (
 	"io"
 	"reflect"
 	"sort"
+	"strings"
 
 	"github.com/benhoyt/goawk/internal/ast"
-	. "github.com/benhoyt/goawk/lexer"
+	"github.com/benhoyt/goawk/lexer"
 )
 
-type resolver struct {
-	// Resolving state
-	funcName string // function name if parsing a func, else ""
-
-	// Variable tracking and resolving
-	locals    map[string]bool                // current function's locals (for determining scope)
-	varTypes  map[string]map[string]typeInfo // map of func name to var name to type
-	varRefs   []varRef                       // all variable references (usually scalars)
-	arrayRefs []arrayRef                     // all array references
-
-	// Function tracking
-	functions   map[string]int // map of function name to index
-	userCalls   []userCall     // record calls so we can resolve them later
-	nativeFuncs map[string]interface{}
-
-	// Configuration and debugging
-	debugTypes  bool      // show variable types for debugging
-	debugWriter io.Writer // where the debug output goes
+// ResolvedProgram is a parsed AWK program plus variable scope and type data
+// prepared by the resolver that is needed for subsequent interpretation.
+type ResolvedProgram struct {
+	ast.Program
+	resolver *resolver
 }
 
-type Config struct {
-	// Enable printing of type information
-	DebugTypes bool
-
-	// io.Writer to print type information on (for example, os.Stderr)
-	DebugWriter io.Writer
-
-	// Map of named Go functions to allow calling from AWK. See docs
-	// on interp.Config.Funcs for details.
-	Funcs map[string]interface{}
+// LookupVar looks up a (possibly-local) variable by function name and
+// variable name, returning its scope, info, and whether it exists.
+func (r *ResolvedProgram) LookupVar(funcName, name string) (Scope, VarInfo, bool) {
+	scope, info, _, exists := r.resolver.lookupVar(funcName, name)
+	return scope, info, exists
 }
 
-func Resolve(prog *ast.Program, config *Config) *ast.ResolvedProgram {
-	r := newResolver(config)
-
-	resolvedProg := &ast.ResolvedProgram{Program: *prog}
-
-	ast.Walk(r, prog)
-
-	r.resolveUserCalls(prog)
-	r.resolveVars(resolvedProg)
-
-	return resolvedProg
+// IterVars iterates over the variables from the given function ("" to iterate
+// globals), calling f for each variable.
+func (r *ResolvedProgram) IterVars(funcName string, f func(name string, info VarInfo)) {
+	for name, info := range r.resolver.varInfo[funcName] {
+		f(name, info)
+	}
 }
 
-func (r *resolver) Visit(node ast.Node) ast.Visitor {
-	switch n := node.(type) {
+// LookupFunc looks up a function by name, returning its info and whether it
+// exists.
+func (r *ResolvedProgram) LookupFunc(name string) (FuncInfo, bool) {
+	info, ok := r.resolver.funcInfo[name]
+	return info, ok
+}
 
-	case *ast.Function:
-		function := n
-		name := function.Name
-		if _, ok := r.functions[name]; ok {
-			panic(ast.PosErrorf(function.Pos, "function %q already defined", name))
-		}
-		r.functions[name] = len(r.functions)
-		r.locals = make(map[string]bool, 7)
-		for _, param := range function.Params {
-			r.locals[param] = true
-		}
-		r.funcName = name
-		r.varTypes[name] = make(map[string]typeInfo)
+// IterFuncs iterates over all the functions, including native (Go-defined)
+// ones, calling f for each function.
+func (r *ResolvedProgram) IterFuncs(f func(name string, info FuncInfo)) {
+	for name, info := range r.resolver.funcInfo {
+		f(name, info)
+	}
+}
 
-		ast.WalkStmtList(r, function.Body)
+// VarInfo holds resolved information about a variable.
+type VarInfo struct {
+	Type  Type
+	Index int
+}
 
-		r.funcName = ""
-		r.locals = nil
+// FuncInfo holds resolved information about a function.
+type FuncInfo struct {
+	Native bool // true if function is a native (Go-defined) function
+	Index  int
+	Params []string // list of parameter names
+}
 
-	case *ast.VarExpr:
-		r.recordVarRef(n)
+// Scope represents the scope of a variable.
+type Scope int
 
-	case *ast.ArrayExpr:
-		r.recordArrayRef(n)
+const (
+	Local   Scope = iota + 1 // locals (function parameters)
+	Special                  // special variables (such as NF)
+	Global                   // globals
+)
 
-	case *ast.UserCallExpr:
-		name := n.Name
-		if r.locals[name] {
-			panic(ast.PosErrorf(n.Pos, "can't call local variable %q as function", name))
-		}
-		for i, arg := range n.Args {
-			ast.Walk(r, arg)
-			r.processUserCallArg(name, arg, i)
-		}
-		r.userCalls = append(r.userCalls, userCall{n, n.Pos, r.funcName})
+func (s Scope) String() string {
+	switch s {
+	case Local:
+		return "local"
+	case Special:
+		return "special"
+	case Global:
+		return "global"
 	default:
-		return r
+		return "unknown scope"
 	}
-
-	return nil
 }
 
-type varType int
+// Type represents the type of a variable: scalar or array.
+type Type int
 
 const (
-	typeUnknown varType = iota
-	typeScalar
-	typeArray
+	unknown Type = iota
+	Scalar
+	Array
 )
 
-func (t varType) String() string {
+func (t Type) String() string {
 	switch t {
-	case typeScalar:
-		return "Scalar"
-	case typeArray:
-		return "Array"
+	case Scalar:
+		return "scalar"
+	case Array:
+		return "array"
 	default:
-		return "Unknown"
+		return "unknown type"
 	}
 }
 
-// typeInfo records type information for a single variable
-type typeInfo struct {
-	typ      varType
-	ref      *ast.VarExpr
-	scope    ast.VarScope
-	index    int
-	callName string
-	argIndex int
-}
+// Config holds resolver configuration.
+type Config struct {
+	// Enable printing of type information
+	DebugTypes bool
 
-// Used by printVarTypes when debugTypes is turned on
-func (t typeInfo) String() string {
-	var scope string
-	switch t.scope {
-	case ast.ScopeGlobal:
-		scope = "Global"
-	case ast.ScopeLocal:
-		scope = "Local"
-	default:
-		scope = "Special"
-	}
-	return fmt.Sprintf("typ=%s ref=%p scope=%s index=%d callName=%q argIndex=%d",
-		t.typ, t.ref, scope, t.index, t.callName, t.argIndex)
-}
+	// io.Writer to print type information on (for example, os.Stderr)
+	DebugWriter io.Writer
 
-// A single variable reference (normally scalar)
-type varRef struct {
-	funcName string
-	ref      *ast.VarExpr
-	isArg    bool
+	// Map of named Go functions to allow calling from AWK. See docs
+	// on interp.Config.Funcs for details.
+	Funcs map[string]interface{}
 }
 
-// A single array reference
-type arrayRef struct {
-	funcName string
-	ref      *ast.ArrayExpr
-}
+// Resolve assigns integer indexes to functions and variables, as well as
+// determining and checking their types (scalar or array).
+func Resolve(prog *ast.Program, config *Config) *ResolvedProgram {
+	if config == nil {
+		config = &Config{}
+	}
 
-// Constructs the resolver
-func newResolver(config *Config) *resolver {
-	r := &resolver{}
-	if config != nil {
-		r.nativeFuncs = config.Funcs
-		r.debugTypes = config.DebugTypes
-		r.debugWriter = config.DebugWriter
+	// First pass determines call graph so we can process functions in
+	// topological order: e.g., if f() calls g(), process g first, then f.
+	callGraph := callGraphVisitor{
+		calls:       make(map[string]map[string]struct{}),
+		funcs:       make(map[string]*ast.Function),
+		funcIndexes: make(map[string]int),
+	}
+	ast.Walk(&callGraph, prog)
+	orderedFuncs := topoSort(callGraph.calls)
+
+	// Ensure functions that weren't called are added to the orderedFuncs list
+	// (order of those doesn't matter, so add them at the end).
+	called := make(map[string]struct{}, len(orderedFuncs))
+	for _, name := range orderedFuncs {
+		called[name] = struct{}{}
+	}
+	for name := range callGraph.funcs {
+		if _, ok := called[name]; !ok {
+			orderedFuncs = append(orderedFuncs, name)
+		}
 	}
-	r.varTypes = make(map[string]map[string]typeInfo)
-	r.varTypes[""] = make(map[string]typeInfo) // globals
-	r.functions = make(map[string]int)
-	initialPos := Position{1, 1}
-	r.recordArrayRef(ast.ArrayRef("ARGV", initialPos))    // interpreter relies on ARGV being present
-	r.recordArrayRef(ast.ArrayRef("ENVIRON", initialPos)) // and other built-in arrays
-	r.recordArrayRef(ast.ArrayRef("FIELDS", initialPos))
-	return r
-}
 
-// Records a call to a user function (for resolving indexes later)
-type userCall struct {
-	call   *ast.UserCallExpr
-	pos    Position
-	inFunc string
-}
+	// Create our type resolver.
+	r := resolver{
+		varInfo:  make(map[string]map[string]VarInfo),
+		funcInfo: make(map[string]FuncInfo),
+		funcs:    callGraph.funcs,
+	}
+	r.varInfo[""] = make(map[string]VarInfo) // func of "" stores global vars
 
-// After parsing, resolve all user calls to their indexes. Also
-// ensures functions called have actually been defined, and that
-// they're not being called with too many arguments.
-func (r *resolver) resolveUserCalls(prog *ast.Program) {
-	// Number the native funcs (order by name to get consistent order)
-	nativeNames := make([]string, 0, len(r.nativeFuncs))
-	for name := range r.nativeFuncs {
+	// Interpreter relies on ARGV and other built-in arrays being present.
+	r.recordVar("", "ARGV", Array, lexer.Position{1, 1})
+	r.recordVar("", "ENVIRON", Array, lexer.Position{1, 1})
+	r.recordVar("", "FIELDS", Array, lexer.Position{1, 1})
+
+	// Assign indexes to native (Go-defined) functions, in order of name.
+	var nativeNames []string
+	for name := range config.Funcs {
 		nativeNames = append(nativeNames, name)
 	}
 	sort.Strings(nativeNames)
-	nativeIndexes := make(map[string]int, len(nativeNames))
 	for i, name := range nativeNames {
-		nativeIndexes[name] = i
+		r.funcInfo[name] = FuncInfo{Native: true, Index: i}
 	}
 
-	for _, c := range r.userCalls {
-		// AWK-defined functions take precedence over native Go funcs
-		index, ok := r.functions[c.call.Name]
-		if !ok {
-			f, haveNative := r.nativeFuncs[c.call.Name]
-			if !haveNative {
-				panic(ast.PosErrorf(c.pos, "undefined function %q", c.call.Name))
-			}
-			typ := reflect.TypeOf(f)
-			if !typ.IsVariadic() && len(c.call.Args) > typ.NumIn() {
-				panic(ast.PosErrorf(c.pos, "%q called with more arguments than declared", c.call.Name))
-			}
-			c.call.Native = true
-			c.call.Index = nativeIndexes[c.call.Name]
-			continue
+	// Main resolver pass: determine types of variables and find function
+	// information. Can't call ast.Walk on prog directly, as it will not
+	// iterate through functions in topological (call graph) order.
+	main := mainVisitor{r: &r, nativeFuncs: config.Funcs}
+	for _, funcName := range orderedFuncs {
+		if funcName == "" {
+			continue // BEGIN, END, and actions are processed below
 		}
-		function := prog.Functions[index]
-		if len(c.call.Args) > len(function.Params) {
-			panic(ast.PosErrorf(c.pos, "%q called with more arguments than declared", c.call.Name))
+		function, exists := callGraph.funcs[funcName]
+		if !exists {
+			// Happens in the case where someone tries to call a local
+			// variable as a function: function f(x) { x() }. That is checked
+			// and flagged as an error in the visitor.
+			continue
 		}
-		c.call.Index = index
+		r.defineFunc(callGraph.funcIndexes[funcName], funcName, function.Params)
+		main.curFunc = funcName
+		ast.WalkStmtList(&main, function.Body)
+		main.curFunc = ""
+	}
+	for _, stmts := range prog.Begin {
+		ast.WalkStmtList(&main, stmts)
+	}
+	for _, action := range prog.Actions {
+		ast.Walk(&main, action)
+	}
+	for _, stmts := range prog.End {
+		ast.WalkStmtList(&main, stmts)
 	}
-}
 
-// For arguments that are variable references, we don't know the
-// type based on context, so mark the types for these as unknown.
-func (r *resolver) processUserCallArg(funcName string, arg ast.Expr, index int) {
-	if varExpr, ok := arg.(*ast.VarExpr); ok {
-		scope, varFuncName := r.getScope(varExpr.Name)
-		ref := r.varTypes[varFuncName][varExpr.Name].ref
-		if ref == varExpr {
-			// Only applies if this is the first reference to this
-			// variable (otherwise we know the type already)
-			r.varTypes[varFuncName][varExpr.Name] = typeInfo{typeUnknown, ref, scope, 0, funcName, index}
+	// For any variables that are still unknown, set their type to scalar.
+	// This can happen for unused variables, such as in the following:
+	//  { f(z) }  function f(x) { print NR }
+	for _, infos := range r.varInfo {
+		for varName, info := range infos {
+			if info.Type == unknown {
+				infos[varName] = VarInfo{Type: Scalar, Index: info.Index}
+			}
 		}
-		// Mark the last related varRef (the most recent one) as a
-		// call argument for later error handling
-		r.varRefs[len(r.varRefs)-1].isArg = true
 	}
-}
 
-// Determine scope of given variable reference (and funcName if it's
-// a local, otherwise empty string)
-func (r *resolver) getScope(name string) (ast.VarScope, string) {
-	switch {
-	case r.locals[name]:
-		return ast.ScopeLocal, r.funcName
-	case ast.SpecialVarIndex(name) > 0:
-		return ast.ScopeSpecial, ""
-	default:
-		return ast.ScopeGlobal, ""
+	// Assign indexes to globals and locals (separate for scalars and arrays).
+	for funcName, infos := range r.varInfo {
+		var names []string
+		if funcName == "" {
+			// For global vars, order indexes by name.
+			for name := range infos {
+				names = append(names, name)
+			}
+			sort.Strings(names)
+		} else {
+			// For local vars, order indexes by parameter order.
+			names = r.funcInfo[funcName].Params
+		}
+		scalar := 0
+		array := 0
+		for _, name := range names {
+			info := infos[name]
+			if info.Type == Array {
+				infos[name] = VarInfo{Type: info.Type, Index: array}
+				array++
+			} else {
+				infos[name] = VarInfo{Type: info.Type, Index: scalar}
+				scalar++
+			}
+		}
 	}
-}
 
-// Record a variable (scalar) reference and return the *VarExpr (but
-// VarExpr.Index won't be set till later)
-func (r *resolver) recordVarRef(expr *ast.VarExpr) {
-	name := expr.Name
-	scope, funcName := r.getScope(name)
-	expr.Scope = scope
-	r.varRefs = append(r.varRefs, varRef{funcName, expr, false})
-	info := r.varTypes[funcName][name]
-	if info.typ == typeUnknown {
-		r.varTypes[funcName][name] = typeInfo{typeScalar, expr, scope, 0, info.callName, 0}
+	if config.DebugTypes {
+		printVarTypes(config.DebugWriter, r.varInfo, r.funcInfo)
 	}
-}
 
-// Record an array reference and return the *ArrayExpr (but
-// ArrayExpr.Index won't be set till later)
-func (r *resolver) recordArrayRef(expr *ast.ArrayExpr) {
-	name := expr.Name
-	scope, funcName := r.getScope(name)
-	if scope == ast.ScopeSpecial {
-		panic(ast.PosErrorf(expr.Pos, "can't use scalar %q as array", name))
-	}
-	expr.Scope = scope
-	r.arrayRefs = append(r.arrayRefs, arrayRef{funcName, expr})
-	info := r.varTypes[funcName][name]
-	if info.typ == typeUnknown {
-		r.varTypes[funcName][name] = typeInfo{typeArray, nil, scope, 0, info.callName, 0}
+	return &ResolvedProgram{
+		Program:  *prog,
+		resolver: &r,
 	}
 }
 
-// Print variable type information (for debugging) on p.debugWriter
-func (r *resolver) printVarTypes(prog *ast.ResolvedProgram) {
-	fmt.Fprintf(r.debugWriter, "scalars: %v\n", prog.Scalars)
-	fmt.Fprintf(r.debugWriter, "arrays: %v\n", prog.Arrays)
-	funcNames := []string{}
-	for funcName := range r.varTypes {
+// Print variable type information (for debugging) on given writer.
+func printVarTypes(w io.Writer, varInfo map[string]map[string]VarInfo, funcInfo map[string]FuncInfo) {
+	var funcNames []string
+	for funcName := range varInfo {
 		funcNames = append(funcNames, funcName)
 	}
 	sort.Strings(funcNames)
 	for _, funcName := range funcNames {
 		if funcName != "" {
-			fmt.Fprintf(r.debugWriter, "function %s\n", funcName)
+			info := funcInfo[funcName]
+			fmt.Fprintf(w, "function %s(%s)  # index %d\n",
+				funcName, strings.Join(info.Params, ", "), info.Index)
 		} else {
-			fmt.Fprintf(r.debugWriter, "globals\n")
+			fmt.Fprintln(w, "globals")
 		}
-		varNames := []string{}
-		for name := range r.varTypes[funcName] {
+		var varNames []string
+		for name := range varInfo[funcName] {
 			varNames = append(varNames, name)
 		}
 		sort.Strings(varNames)
 		for _, name := range varNames {
-			info := r.varTypes[funcName][name]
-			fmt.Fprintf(r.debugWriter, "  %s: %s\n", name, info)
+			info := varInfo[funcName][name]
+			fmt.Fprintf(w, "  %s: %s %d\n", name, info.Type, info.Index)
 		}
 	}
 }
 
-// Resolve unknown variables types and generate variable indexes and
-// name-to-index mappings for interpreter
-func (r *resolver) resolveVars(prog *ast.ResolvedProgram) {
-	// First go through all unknown types and try to determine the
-	// type from the parameter type in that function definition.
-	// Iterate through functions in topological order, for example
-	// if f() calls g(), process g first, then f.
-	callGraph := make(map[string]map[string]struct{})
-	for _, call := range r.userCalls {
-		if _, ok := callGraph[call.inFunc]; !ok {
-			callGraph[call.inFunc] = make(map[string]struct{})
+// resolver tracks variable scopes and types as well as function information.
+type resolver struct {
+	varInfo  map[string]map[string]VarInfo
+	funcInfo map[string]FuncInfo
+	funcs    map[string]*ast.Function
+}
+
+// Look up variable from function funcName and return its scope and type
+// information, the function it was defined in, and whether it exists.
+func (r *resolver) lookupVar(funcName, varName string) (scope Scope, info VarInfo, varFunc string, exists bool) {
+	// If inside a function, try looking for a local variable first.
+	if funcName != "" {
+		if info, exists = r.varInfo[funcName][varName]; exists {
+			return Local, info, funcName, true
 		}
-		callGraph[call.inFunc][call.call.Name] = struct{}{}
 	}
-	sortedFuncs := topoSort(callGraph)
-	for _, funcName := range sortedFuncs {
-		infos := r.varTypes[funcName]
-		for name, info := range infos {
-			if info.scope == ast.ScopeSpecial || info.typ != typeUnknown {
-				// It's a special var or type is already known
-				continue
-			}
-			funcIndex, ok := r.functions[info.callName]
-			if !ok {
-				// Function being called is a native function
-				continue
-			}
-			// Determine var type based on type of this parameter
-			// in the called function (if we know that)
-			paramName := prog.Functions[funcIndex].Params[info.argIndex]
-			typ := r.varTypes[info.callName][paramName].typ
-			if typ != typeUnknown {
-				if r.debugTypes {
-					fmt.Fprintf(r.debugWriter, "resolving %s:%s to %s\n",
-						funcName, name, typ)
-				}
-				info.typ = typ
-				r.varTypes[funcName][name] = info
-			}
-		}
+	// Next try looking for a special variable (such as NR).
+	index := ast.SpecialVarIndex(varName)
+	if index > 0 {
+		// Special variables are all scalar (ARGV and similar are done as
+		// regular arrays).
+		return Special, VarInfo{Type: Scalar, Index: index}, "", true
+	}
+	// Then try looking for a global variable.
+	if info, exists = r.varInfo[""][varName]; exists {
+		return Global, info, "", true
 	}
+	return 0, VarInfo{}, "", false // not defined at all
+}
 
-	// Resolve global variables (iteration order is undefined, so
-	// assign indexes basically randomly)
-	prog.Scalars = make(map[string]int)
-	prog.Arrays = make(map[string]int)
-	for name, info := range r.varTypes[""] {
-		_, isFunc := r.functions[name]
-		if isFunc {
-			// Global var can't also be the name of a function
-			pos := Position{1, 1} // Ideally it'd be the position of the global var.
-			panic(ast.PosErrorf(pos, "global var %q can't also be a function", name))
-		}
-		var index int
-		if info.scope == ast.ScopeSpecial {
-			index = ast.SpecialVarIndex(name)
-		} else if info.typ == typeArray {
-			index = len(prog.Arrays)
-			prog.Arrays[name] = index
-		} else {
-			index = len(prog.Scalars)
-			prog.Scalars[name] = index
+// Record that the given variable (in function funcName) is of the given type.
+func (r *resolver) recordVar(funcName, varName string, typ Type, pos lexer.Position) {
+	_, info, varFunc, exists := r.lookupVar(funcName, varName)
+	if !exists {
+		// Doesn't exist as a local or a global, add it as a new global.
+		r.varInfo[""][varName] = VarInfo{Type: typ}
+		if _, isFunc := r.funcs[varName]; isFunc {
+			panic(ast.PosErrorf(pos, "global var %q can't also be a function", varName))
 		}
-		info.index = index
-		r.varTypes[""][name] = info
+		return
+	}
+	if info.Type != typ && info.Type != unknown && typ != unknown {
+		panic(ast.PosErrorf(pos, "can't use %s %q as %s", info.Type, varName, typ))
 	}
+	if info.Type == unknown {
+		r.varInfo[varFunc][varName] = VarInfo{Type: typ, Index: info.Index}
+	}
+}
 
-	// Fill in unknown parameter types that are being called with arrays,
-	// for example, as in the following code:
-	//
-	// BEGIN { arr[0]; f(arr) }
-	// function f(a) { }
-	for _, c := range r.userCalls {
-		if c.call.Native {
-			continue
+// Define a function and its local variables.
+func (r *resolver) defineFunc(index int, name string, params []string) {
+	r.funcInfo[name] = FuncInfo{Native: false, Index: index, Params: params}
+	// Define the local variable names (we don't know their types yet).
+	r.varInfo[name] = make(map[string]VarInfo)
+	for _, param := range params {
+		r.varInfo[name][param] = VarInfo{}
+	}
+}
+
+// callGraphVisitor records what functions are called by the current function
+// to build our call graph.
+type callGraphVisitor struct {
+	calls       map[string]map[string]struct{} // map of current function to called function
+	funcs       map[string]*ast.Function
+	funcIndexes map[string]int
+	curFunc     string
+}
+
+func (v *callGraphVisitor) Visit(node ast.Node) ast.Visitor {
+	switch n := node.(type) {
+	case *ast.Function:
+		if _, ok := v.funcs[n.Name]; ok {
+			panic(ast.PosErrorf(n.Pos, "function %q already defined", n.Name))
 		}
-		function := prog.Functions[c.call.Index]
-		for i, arg := range c.call.Args {
-			varExpr, ok := arg.(*ast.VarExpr)
-			if !ok {
-				continue
-			}
-			funcName := r.getVarFuncName(prog, varExpr.Name, c.inFunc)
-			argType := r.varTypes[funcName][varExpr.Name]
-			paramType := r.varTypes[function.Name][function.Params[i]]
-			if argType.typ == typeArray && paramType.typ == typeUnknown {
-				paramType.typ = argType.typ
-				r.varTypes[function.Name][function.Params[i]] = paramType
-			}
+		v.funcs[n.Name] = n
+		v.funcIndexes[n.Name] = len(v.funcIndexes)
+		v.curFunc = n.Name
+		ast.WalkStmtList(v, n.Body)
+		v.curFunc = ""
+
+	case *ast.UserCallExpr:
+		if _, ok := v.calls[v.curFunc]; !ok {
+			v.calls[v.curFunc] = make(map[string]struct{})
 		}
+		v.calls[v.curFunc][n.Name] = struct{}{}
+		ast.WalkExprList(v, n.Args)
+
+	default:
+		return v
 	}
+	return nil
+}
 
-	// Resolve local variables (assign indexes in order of params).
-	// Also patch up Function.Arrays (tells interpreter which args
-	// are arrays).
-	for funcName, infos := range r.varTypes {
-		if funcName == "" {
-			continue
+// mainVisitor records types of variables and performs various checks.
+type mainVisitor struct {
+	r           *resolver
+	nativeFuncs map[string]interface{}
+	curFunc     string
+}
+
+func (v *mainVisitor) Visit(node ast.Node) ast.Visitor {
+	switch n := node.(type) {
+	case *ast.VarExpr:
+		v.r.recordVar(v.curFunc, n.Name, Scalar, n.Pos)
+
+	case *ast.ForInStmt:
+		v.r.recordVar(v.curFunc, n.Var, Scalar, n.VarPos)
+		v.r.recordVar(v.curFunc, n.Array, Array, n.ArrayPos)
+		ast.WalkStmtList(v, n.Body)
+
+	case *ast.IndexExpr:
+		ast.WalkExprList(v, n.Index)
+		v.r.recordVar(v.curFunc, n.Array, Array, n.ArrayPos)
+
+	case *ast.InExpr:
+		ast.WalkExprList(v, n.Index)
+		v.r.recordVar(v.curFunc, n.Array, Array, n.ArrayPos)
+
+	case *ast.DeleteStmt:
+		v.r.recordVar(v.curFunc, n.Array, Array, n.ArrayPos)
+		ast.WalkExprList(v, n.Index)
+
+	case *ast.CallExpr:
+		switch n.Func {
+		case lexer.F_SPLIT:
+			ast.Walk(v, n.Args[0])
+			varExpr := n.Args[1].(*ast.VarExpr) // split()'s 2nd arg is always an array
+			v.r.recordVar(v.curFunc, varExpr.Name, Array, varExpr.Pos)
+			ast.WalkExprList(v, n.Args[2:])
+		default:
+			ast.WalkExprList(v, n.Args)
 		}
-		scalarIndex := 0
-		arrayIndex := 0
-		functionIndex := r.functions[funcName]
-		function := prog.Functions[functionIndex]
-		arrays := make([]bool, len(function.Params))
-		for i, name := range function.Params {
-			info := infos[name]
-			var index int
-			if info.typ == typeArray {
-				index = arrayIndex
-				arrayIndex++
-				arrays[i] = true
-			} else {
-				// typeScalar or typeUnknown: variables may still be
-				// of unknown type if they've never been referenced --
-				// default to scalar in that case
-				index = scalarIndex
-				scalarIndex++
-			}
-			info.index = index
-			r.varTypes[funcName][name] = info
+
+	case *ast.UserCallExpr:
+		_, _, varFunc, exists := v.r.lookupVar(v.curFunc, n.Name)
+		if varFunc != "" && exists {
+			panic(ast.PosErrorf(n.Pos, "can't call local variable %q as function", n.Name))
 		}
-		prog.Functions[functionIndex].Arrays = arrays
-	}
 
-	// Check that variables passed to functions are the correct type
-	for _, c := range r.userCalls {
-		// Check native function calls
-		if c.call.Native {
-			for _, arg := range c.call.Args {
-				varExpr, ok := arg.(*ast.VarExpr)
-				if !ok {
-					// Non-variable expression, must be scalar
-					continue
-				}
-				funcName := r.getVarFuncName(prog, varExpr.Name, c.inFunc)
-				info := r.varTypes[funcName][varExpr.Name]
-				if info.typ == typeArray {
-					panic(ast.PosErrorf(c.pos, "can't pass array %q to native function", varExpr.Name))
-				}
+		funcInfo, exists := v.r.funcInfo[n.Name]
+		if !exists {
+			panic(ast.PosErrorf(n.Pos, "undefined function %q", n.Name))
+		}
+
+		numParams := len(funcInfo.Params)
+		if funcInfo.Native {
+			typ := reflect.TypeOf(v.nativeFuncs[n.Name])
+			numParams = typ.NumIn()
+			if typ.IsVariadic() {
+				numParams = 1000000000 // bigger than any reasonable len(n.Args) value!
 			}
-			continue
+		}
+		if len(n.Args) > numParams {
+			panic(ast.PosErrorf(n.Pos, "%q called with more arguments than declared", n.Name))
 		}
 
-		// Check AWK function calls
-		function := prog.Functions[c.call.Index]
-		for i, arg := range c.call.Args {
+		for i, arg := range n.Args {
 			varExpr, ok := arg.(*ast.VarExpr)
 			if !ok {
-				if function.Arrays[i] {
-					panic(ast.PosErrorf(c.pos, "can't pass scalar %s as array param", arg))
+				// Argument is not a variable, process normally.
+				if !funcInfo.Native {
+					paramInfo := v.r.varInfo[n.Name][funcInfo.Params[i]] // type info of corresponding parameter
+					if paramInfo.Type == Array {
+						panic(ast.PosErrorf(n.Pos, "can't pass scalar %s as array param", arg))
+					}
 				}
+				ast.Walk(v, arg)
 				continue
 			}
-			funcName := r.getVarFuncName(prog, varExpr.Name, c.inFunc)
-			info := r.varTypes[funcName][varExpr.Name]
-			if info.typ == typeArray && !function.Arrays[i] {
-				panic(ast.PosErrorf(c.pos, "can't pass array %q as scalar param", varExpr.Name))
-			}
-			if info.typ != typeArray && function.Arrays[i] {
-				panic(ast.PosErrorf(c.pos, "can't pass scalar %q as array param", varExpr.Name))
-			}
-		}
-	}
 
-	if r.debugTypes {
-		r.printVarTypes(prog)
-	}
+			if funcInfo.Native {
+				// Arguments to native function can only be scalar.
+				v.r.recordVar(v.curFunc, varExpr.Name, Scalar, varExpr.Pos)
+				continue
+			}
 
-	// Patch up variable indexes (interpreter uses an index instead
-	// of name for more efficient lookups)
-	for _, varRef := range r.varRefs {
-		info := r.varTypes[varRef.funcName][varRef.ref.Name]
-		if info.typ == typeArray && !varRef.isArg {
-			panic(ast.PosErrorf(varRef.ref.Pos, "can't use array %q as scalar", varRef.ref.Name))
-		}
-		varRef.ref.Index = info.index
-	}
-	for _, arrayRef := range r.arrayRefs {
-		info := r.varTypes[arrayRef.funcName][arrayRef.ref.Name]
-		if info.typ == typeScalar {
-			panic(ast.PosErrorf(arrayRef.ref.Pos, "can't use scalar %q as array", arrayRef.ref.Name))
+			// Variable passed to AWK-defined function may be scalar or array,
+			// determine from how it was used elsewhere.
+			paramName := funcInfo.Params[i]             // name of corresponding parameter
+			paramInfo := v.r.varInfo[n.Name][paramName] // type info of parameter
+			_, varInfo, _, _ := v.r.lookupVar(v.curFunc, varExpr.Name)
+			switch {
+			case varInfo.Type == unknown && paramInfo.Type != unknown:
+				// Variable's type is not known but param type is, set variable type.
+				v.r.recordVar(v.curFunc, varExpr.Name, paramInfo.Type, varExpr.Pos)
+			case varInfo.Type != unknown && paramInfo.Type == unknown:
+				// Variable's type is known but param type is not, set param type.
+				funcPos := v.r.funcs[n.Name].Pos // best position we have at this point
+				v.r.recordVar(n.Name, paramName, varInfo.Type, funcPos)
+			case varInfo.Type != paramInfo.Type && varInfo.Type != unknown && paramInfo.Type != unknown:
+				// Both types are known but don't match -- type error!
+				panic(ast.PosErrorf(varExpr.Pos, "can't pass %s %q as %s param",
+					varInfo.Type, varExpr.Name, paramInfo.Type))
+			default:
+				// Ensure variable references are recorded, even if the type
+				// is not yet known.
+				v.r.recordVar(v.curFunc, varExpr.Name, unknown, varExpr.Pos)
+			}
 		}
-		arrayRef.ref.Index = info.index
-	}
-}
 
-// If name refers to a local (in function inFunc), return that
-// function's name, otherwise return "" (meaning global).
-func (r *resolver) getVarFuncName(prog *ast.ResolvedProgram, name, inFunc string) string {
-	if inFunc == "" {
-		return ""
-	}
-	for _, param := range prog.Functions[r.functions[inFunc]].Params {
-		if name == param {
-			return inFunc
-		}
+	default:
+		return v
 	}
-	return ""
+	return nil
 }
diff --git a/interp/functions.go b/interp/functions.go
index 4eff792..e9610cf 100644
--- a/interp/functions.go
+++ b/interp/functions.go
@@ -12,7 +12,7 @@ import (
 	"strings"
 	"unicode/utf8"
 
-	"github.com/benhoyt/goawk/internal/ast"
+	"github.com/benhoyt/goawk/internal/resolver"
 	. "github.com/benhoyt/goawk/lexer"
 )
 
@@ -242,7 +242,7 @@ func validNativeType(typ reflect.Type) bool {
 }
 
 // Guts of the split() function
-func (p *interp) split(s string, scope ast.VarScope, index int, fs string) (int, error) {
+func (p *interp) split(s string, scope resolver.Scope, index int, fs string) (int, error) {
 	var parts []string
 	if fs == " " {
 		parts = strings.Fields(s)
diff --git a/interp/interp.go b/interp/interp.go
index ad1f3fb..97f41a2 100644
--- a/interp/interp.go
+++ b/interp/interp.go
@@ -30,6 +30,7 @@ import (
 
 	"github.com/benhoyt/goawk/internal/ast"
 	"github.com/benhoyt/goawk/internal/compiler"
+	"github.com/benhoyt/goawk/internal/resolver"
 	"github.com/benhoyt/goawk/parser"
 )
 
@@ -91,14 +92,16 @@ type interp struct {
 	noArgVars     bool
 
 	// Scalars, arrays, and function state
-	globals     []value
-	stack       []value
-	sp          int
-	frame       []value
-	arrays      []map[string]value
-	localArrays [][]int
-	callDepth   int
-	nativeFuncs []nativeFunc
+	globals       []value
+	stack         []value
+	sp            int
+	frame         []value
+	arrays        []map[string]value
+	localArrays   [][]int
+	callDepth     int
+	nativeFuncs   []nativeFunc
+	scalarIndexes map[string]int
+	arrayIndexes  map[string]int
 
 	// File, line, and field handling
 	filename        value
@@ -359,10 +362,19 @@ func newInterp(program *parser.Program) *interp {
 	}
 
 	// Allocate memory for variables and virtual machine stack
-	p.globals = make([]value, len(program.Scalars))
+	p.scalarIndexes = make(map[string]int)
+	p.arrayIndexes = make(map[string]int)
+	program.IterVars("", func(name string, info resolver.VarInfo) {
+		if info.Type == resolver.Array {
+			p.arrayIndexes[name] = info.Index
+		} else {
+			p.scalarIndexes[name] = info.Index
+		}
+	})
+	p.globals = make([]value, len(p.scalarIndexes))
 	p.stack = make([]value, initialStackSize)
-	p.arrays = make([]map[string]value, len(program.Arrays), len(program.Arrays)+initialStackSize)
-	for i := 0; i < len(program.Arrays); i++ {
+	p.arrays = make([]map[string]value, len(p.arrayIndexes), len(p.arrayIndexes)+initialStackSize)
+	for i := 0; i < len(p.arrayIndexes); i++ {
 		p.arrays[i] = make(map[string]value)
 	}
 
@@ -434,11 +446,11 @@ func (p *interp) setExecuteConfig(config *Config) error {
 	}
 
 	// Set up ARGV and other variables from config
-	argvIndex := p.program.Arrays["ARGV"]
-	p.setArrayValue(ast.ScopeGlobal, argvIndex, "0", str(config.Argv0))
+	argvIndex := p.arrayIndexes["ARGV"]
+	p.setArrayValue(resolver.Global, argvIndex, "0", str(config.Argv0))
 	p.argc = len(config.Args) + 1
 	for i, arg := range config.Args {
-		p.setArrayValue(ast.ScopeGlobal, argvIndex, strconv.Itoa(i+1), numStr(arg))
+		p.setArrayValue(resolver.Global, argvIndex, strconv.Itoa(i+1), numStr(arg))
 	}
 	p.noArgVars = config.NoArgVars
 	p.filenameIndex = 1
@@ -461,16 +473,16 @@ func (p *interp) setExecuteConfig(config *Config) error {
 	}
 
 	// Set up ENVIRON from config or environment variables
-	environIndex := p.program.Arrays["ENVIRON"]
+	environIndex := p.arrayIndexes["ENVIRON"]
 	if config.Environ != nil {
 		for i := 0; i < len(config.Environ); i += 2 {
-			p.setArrayValue(ast.ScopeGlobal, environIndex, config.Environ[i], numStr(config.Environ[i+1]))
+			p.setArrayValue(resolver.Global, environIndex, config.Environ[i], numStr(config.Environ[i+1]))
 		}
 	} else {
 		for _, kv := range os.Environ() {
 			eq := strings.IndexByte(kv, '=')
 			if eq >= 0 {
-				p.setArrayValue(ast.ScopeGlobal, environIndex, kv[:eq], numStr(kv[eq+1:]))
+				p.setArrayValue(resolver.Global, environIndex, kv[:eq], numStr(kv[eq+1:]))
 			}
 		}
 	}
@@ -728,7 +740,7 @@ func (p *interp) setVarByName(name, value string) error {
 	if index > 0 {
 		return p.setSpecial(index, numStr(value))
 	}
-	index, ok := p.program.Scalars[name]
+	index, ok := p.scalarIndexes[name]
 	if ok {
 		p.globals[index] = numStr(value)
 		return nil
@@ -838,8 +850,8 @@ func (p *interp) setSpecial(index int, v value) error {
 // Determine the index of given array into the p.arrays slice. Global
 // arrays are just at p.arrays[index], local arrays have to be looked
 // up indirectly.
-func (p *interp) arrayIndex(scope ast.VarScope, index int) int {
-	if scope == ast.ScopeGlobal {
+func (p *interp) arrayIndex(scope resolver.Scope, index int) int {
+	if scope == resolver.Global {
 		return index
 	} else {
 		return p.localArrays[len(p.localArrays)-1][index]
@@ -847,7 +859,7 @@ func (p *interp) arrayIndex(scope ast.VarScope, index int) int {
 }
 
 // Return array with given scope and index.
-func (p *interp) array(scope ast.VarScope, index int) map[string]value {
+func (p *interp) array(scope resolver.Scope, index int) map[string]value {
 	return p.arrays[p.arrayIndex(scope, index)]
 }
 
@@ -857,7 +869,7 @@ func (p *interp) localArray(index int) map[string]value {
 }
 
 // Set a value in given array by key (index)
-func (p *interp) setArrayValue(scope ast.VarScope, arrayIndex int, index string, v value) {
+func (p *interp) setArrayValue(scope resolver.Scope, arrayIndex int, index string, v value) {
 	array := p.array(scope, arrayIndex)
 	array[index] = v
 }
diff --git a/interp/io.go b/interp/io.go
index ec23c21..bda14ca 100644
--- a/interp/io.go
+++ b/interp/io.go
@@ -17,7 +17,7 @@ import (
 	"strings"
 	"unicode/utf8"
 
-	"github.com/benhoyt/goawk/internal/ast"
+	"github.com/benhoyt/goawk/internal/resolver"
 	. "github.com/benhoyt/goawk/lexer"
 )
 
@@ -294,7 +294,7 @@ func (p *interp) setFieldNames(names []string) {
 	p.fieldIndexes = nil // clear name-to-index cache
 
 	// Populate FIELDS array (mapping of field indexes to field names).
-	fieldsArray := p.array(ast.ScopeGlobal, p.program.Arrays["FIELDS"])
+	fieldsArray := p.array(resolver.Global, p.arrayIndexes["FIELDS"])
 	for k := range fieldsArray {
 		delete(fieldsArray, k)
 	}
@@ -732,8 +732,8 @@ func (p *interp) nextLine() (string, error) {
 				// getArrayValue() here as it would set the value if
 				// not present
 				index := strconv.Itoa(p.filenameIndex)
-				argvIndex := p.program.Arrays["ARGV"]
-				argvArray := p.array(ast.ScopeGlobal, argvIndex)
+				argvIndex := p.arrayIndexes["ARGV"]
+				argvArray := p.array(resolver.Global, argvIndex)
 				filename := p.toString(argvArray[index])
 				p.filenameIndex++
 
diff --git a/interp/newexecute.go b/interp/newexecute.go
index ba3caed..83287fd 100644
--- a/interp/newexecute.go
+++ b/interp/newexecute.go
@@ -6,7 +6,7 @@ import (
 	"context"
 	"math"
 
-	"github.com/benhoyt/goawk/internal/ast"
+	"github.com/benhoyt/goawk/internal/resolver"
 	"github.com/benhoyt/goawk/parser"
 )
 
@@ -65,11 +65,11 @@ func (p *Interpreter) Execute(config *Config) (int, error) {
 // numbers are included as type float64, strings (including "numeric strings")
 // are included as type string. If the named array does not exist, return nil.
 func (p *Interpreter) Array(name string) map[string]interface{} {
-	index, exists := p.interp.program.Arrays[name]
+	index, exists := p.interp.arrayIndexes[name]
 	if !exists {
 		return nil
 	}
-	array := p.interp.array(ast.ScopeGlobal, index)
+	array := p.interp.array(resolver.Global, index)
 	result := make(map[string]interface{}, len(array))
 	for k, v := range array {
 		switch v.typ {
diff --git a/interp/vm.go b/interp/vm.go
index 3da5588..feb7b5f 100644
--- a/interp/vm.go
+++ b/interp/vm.go
@@ -10,8 +10,8 @@ import (
 	"strings"
 	"time"
 
-	"github.com/benhoyt/goawk/internal/ast"
 	"github.com/benhoyt/goawk/internal/compiler"
+	"github.com/benhoyt/goawk/internal/resolver"
 	"github.com/benhoyt/goawk/lexer"
 )
 
@@ -180,7 +180,7 @@ func (p *interp) execute(code []compiler.Opcode) error {
 			arrayScope := code[ip]
 			arrayIndex := code[ip+1]
 			ip += 2
-			array := p.array(ast.VarScope(arrayScope), int(arrayIndex))
+			array := p.array(resolver.Scope(arrayScope), int(arrayIndex))
 			index := p.toString(p.pop())
 			delete(array, index)
 
@@ -188,7 +188,7 @@ func (p *interp) execute(code []compiler.Opcode) error {
 			arrayScope := code[ip]
 			arrayIndex := code[ip+1]
 			ip += 2
-			array := p.array(ast.VarScope(arrayScope), int(arrayIndex))
+			array := p.array(resolver.Scope(arrayScope), int(arrayIndex))
 			for k := range array {
 				delete(array, k)
 			}
@@ -602,15 +602,15 @@ func (p *interp) execute(code []compiler.Opcode) error {
 			arrayIndex := code[ip+3]
 			offset := code[ip+4]
 			ip += 5
-			array := p.array(ast.VarScope(arrayScope), int(arrayIndex))
+			array := p.array(resolver.Scope(arrayScope), int(arrayIndex))
 			loopCode := code[ip : ip+int(offset)]
 			for index := range array {
-				switch ast.VarScope(varScope) {
-				case ast.ScopeGlobal:
+				switch resolver.Scope(varScope) {
+				case resolver.Global:
 					p.globals[varIndex] = str(index)
-				case ast.ScopeLocal:
+				case resolver.Local:
 					p.frame[varIndex] = str(index)
-				default: // ScopeSpecial
+				default: // resolver.Special
 					err := p.setSpecial(int(varIndex), str(index))
 					if err != nil {
 						return err
@@ -642,7 +642,7 @@ func (p *interp) execute(code []compiler.Opcode) error {
 			arrayIndex := code[ip+1]
 			ip += 2
 			s := p.toString(p.peekTop())
-			n, err := p.split(s, ast.VarScope(arrayScope), int(arrayIndex), p.fieldSep)
+			n, err := p.split(s, resolver.Scope(arrayScope), int(arrayIndex), p.fieldSep)
 			if err != nil {
 				return err
 			}
@@ -653,7 +653,7 @@ func (p *interp) execute(code []compiler.Opcode) error {
 			arrayIndex := code[ip+1]
 			ip += 2
 			s, fieldSep := p.peekPop()
-			n, err := p.split(p.toString(s), ast.VarScope(arrayScope), int(arrayIndex), p.toString(fieldSep))
+			n, err := p.split(p.toString(s), resolver.Scope(arrayScope), int(arrayIndex), p.toString(fieldSep))
 			if err != nil {
 				return err
 			}
@@ -686,7 +686,7 @@ func (p *interp) execute(code []compiler.Opcode) error {
 			// Handle array arguments
 			var arrays []int
 			for j := 0; j < numArrayArgs; j++ {
-				arrayScope := ast.VarScope(code[ip])
+				arrayScope := resolver.Scope(code[ip])
 				arrayIndex := int(code[ip+1])
 				ip += 2
 				arrays = append(arrays, p.arrayIndex(arrayScope, arrayIndex))
@@ -883,7 +883,7 @@ func (p *interp) execute(code []compiler.Opcode) error {
 			}
 			index := p.toString(p.peekTop())
 			if ret == 1 {
-				array := p.array(ast.VarScope(arrayScope), int(arrayIndex))
+				array := p.array(resolver.Scope(arrayScope), int(arrayIndex))
 				array[index] = numStr(line)
 			}
 			p.replaceTop(num(ret))
diff --git a/parser/parser.go b/parser/parser.go
index d449030..4c3b855 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -83,9 +83,8 @@ func ParseProgram(src []byte, config *ParserConfig) (prog *Program, err error) {
 	// Parse into abstract syntax tree
 	astProg := p.program()
 
+	// Resolve variable scopes and types
 	prog = &Program{}
-
-	// Resolve step
 	prog.ResolvedProgram = *resolver.Resolve(astProg, config.toResolverConfig())
 
 	// Compile to virtual machine code
@@ -99,7 +98,7 @@ type Program struct {
 	// but are exported for the interpreter (Program itself needs to
 	// be exported in package "parser", otherwise these could live in
 	// "internal/ast".)
-	ast.ResolvedProgram
+	resolver.ResolvedProgram
 	Compiled *compiler.Program
 }
 
@@ -259,8 +258,7 @@ func (p *parser) simpleStmt() ast.Stmt {
 		}
 	case DELETE:
 		p.next()
-		ref := ast.ArrayRef(p.val, p.pos)
-		p.expect(NAME)
+		name, namePos := p.expectName()
 		var index []ast.Expr
 		if p.tok == LBRACKET {
 			p.next()
@@ -270,7 +268,7 @@ func (p *parser) simpleStmt() ast.Stmt {
 			}
 			p.expect(RBRACKET)
 		}
-		return &ast.DeleteStmt{ref, index, startPos, p.pos}
+		return &ast.DeleteStmt{name, namePos, index, startPos, p.pos}
 	case IF, FOR, WHILE, DO, BREAK, CONTINUE, NEXT, NEXTFILE, EXIT, RETURN:
 		panic(p.errorf("expected print/printf, delete, or expression"))
 	default:
@@ -337,7 +335,16 @@ func (p *parser) stmt() ast.Stmt {
 			}
 			bodyStart := p.pos
 			body := p.loopStmts()
-			s = &ast.ForInStmt{varExpr, inExpr.Array, bodyStart, body, startPos, p.pos}
+			s = &ast.ForInStmt{
+				Var:       varExpr.Name,
+				VarPos:    varExpr.Pos,
+				Array:     inExpr.Array,
+				ArrayPos:  inExpr.ArrayPos,
+				BodyStart: bodyStart,
+				Body:      body,
+				Start:     startPos,
+				End:       p.pos,
+			}
 		} else {
 			// Match: for ([pre]; [cond]; [post]) body
 			p.expect(SEMICOLON)
@@ -453,9 +460,7 @@ func (p *parser) function() *ast.Function {
 		panic(p.errorf("can't nest functions"))
 	}
 	p.next()
-	name := p.val
-	funcNamePos := p.pos
-	p.expect(NAME)
+	name, funcNamePos := p.expectName()
 	p.expect(LPAREN)
 	first := true
 	params := make([]string, 0, 7) // pre-allocate some to reduce allocations
@@ -486,7 +491,7 @@ func (p *parser) function() *ast.Function {
 
 	p.funcName = ""
 
-	return &ast.Function{name, params, nil, body, funcNamePos}
+	return &ast.Function{name, params, body, funcNamePos}
 }
 
 // Parse expressions separated by commas: args to print[f] or user
@@ -629,9 +634,8 @@ func (p *parser) _in(higher func() ast.Expr) ast.Expr {
 	expr := higher()
 	for p.tok == IN {
 		p.next()
-		ref := ast.ArrayRef(p.val, p.pos)
-		p.expect(NAME)
-		expr = &ast.InExpr{[]ast.Expr{expr}, ref}
+		name, namePos := p.expectName()
+		expr = &ast.InExpr{[]ast.Expr{expr}, name, namePos}
 	}
 	return expr
 }
@@ -746,9 +750,7 @@ func (p *parser) primary() ast.Expr {
 		}
 		return &ast.IncrExpr{expr, op, true}
 	case NAME:
-		name := p.val
-		namePos := p.pos
-		p.next()
+		name, namePos := p.expectName()
 		if p.tok == LBRACKET {
 			// a[x] or a[x, y] array index expression
 			p.next()
@@ -757,14 +759,14 @@ func (p *parser) primary() ast.Expr {
 				panic(p.errorf("expected expression instead of ]"))
 			}
 			p.expect(RBRACKET)
-			return &ast.IndexExpr{ast.ArrayRef(name, namePos), index}
+			return &ast.IndexExpr{name, namePos, index}
 		} else if p.tok == LPAREN && !p.lexer.HadSpace() {
 			// Grammar requires no space between function name and
 			// left paren for user function calls, hence the funky
 			// lexer.HadSpace() method.
 			return p.userCall(name, namePos)
 		}
-		return ast.VarRef(name, namePos)
+		return &ast.VarExpr{name, namePos}
 	case LPAREN:
 		parenPos := p.pos
 		p.next()
@@ -780,9 +782,8 @@ func (p *parser) primary() ast.Expr {
 			p.expect(RPAREN)
 			if p.tok == IN {
 				p.next()
-				ref := ast.ArrayRef(p.val, p.pos)
-				p.expect(NAME)
-				return &ast.InExpr{exprs, ref}
+				name, namePos := p.expectName()
+				return &ast.InExpr{exprs, name, namePos}
 			}
 			// MultiExpr is used as a pseudo-expression for print[f] parsing.
 			return p.multiExpr(exprs, parenPos)
@@ -824,9 +825,8 @@ func (p *parser) primary() ast.Expr {
 		p.expect(LPAREN)
 		str := p.expr()
 		p.commaNewlines()
-		ref := ast.ArrayRef(p.val, p.pos)
-		p.expect(NAME)
-		args := []ast.Expr{str, ref}
+		name, namePos := p.expectName()
+		args := []ast.Expr{str, &ast.VarExpr{name, namePos}}
 		if p.tok == COMMA {
 			p.commaNewlines()
 			args = append(args, p.regexStr(p.expr))
@@ -930,9 +930,7 @@ func (p *parser) optionalLValue() ast.Expr {
 			// User function call, e.g., foo() not lvalue.
 			return nil
 		}
-		name := p.val
-		namePos := p.pos
-		p.next()
+		name, namePos := p.expectName()
 		if p.tok == LBRACKET {
 			// a[x] or a[x, y] array index expression
 			p.next()
@@ -941,9 +939,9 @@ func (p *parser) optionalLValue() ast.Expr {
 				panic(p.errorf("expected expression instead of ]"))
 			}
 			p.expect(RBRACKET)
-			return &ast.IndexExpr{ast.ArrayRef(name, namePos), index}
+			return &ast.IndexExpr{name, namePos, index}
 		}
-		return ast.VarRef(name, namePos)
+		return &ast.VarExpr{name, namePos}
 	case DOLLAR:
 		p.next()
 		return &ast.FieldExpr{p.primary()}
@@ -1031,6 +1029,13 @@ func (p *parser) expect(tok Token) {
 	p.next()
 }
 
+// Ensure current token is a name, parse it, and return name and position.
+func (p *parser) expectName() (string, Position) {
+	name, pos := p.val, p.pos
+	p.expect(NAME)
+	return name, pos
+}
+
 // Return true iff current token matches one of the given operators,
 // but don't parse next token.
 func (p *parser) matches(operators ...Token) bool {
@@ -1063,7 +1068,7 @@ func (p *parser) userCall(name string, pos Position) *ast.UserCallExpr {
 		i++
 	}
 	p.expect(RPAREN)
-	return ast.UserCall(name, args, pos)
+	return &ast.UserCallExpr{name, args, pos}
 }
 
 // Record a "multi expression" (comma-separated pseudo-expression


Test patch:
diff --git a/goawk_test.go b/goawk_test.go
index fd86df2..3d4fdf3 100644
--- a/goawk_test.go
+++ b/goawk_test.go
@@ -625,7 +625,25 @@ func TestGoAWKSpecificOptions(t *testing.T) {
 		{[]string{"-oxyz", `{}`}, "", "", "invalid output mode \"xyz\"\n"},
 		{[]string{"-H", `{}`}, "", "", "-H only allowed together with -i\n"},
 
-		// Debug options (don't test -dt as its output is not stable)
+		// Debug options
+		{[]string{"-dt", `
+BEGIN { x=42; a[1]=x; print f(a, 1) }
+function f(b, y, z) { return b[y+z] }
+function hi(x) { print "hi " x }
+`}, "", `
+globals
+  ARGV: array 0
+  ENVIRON: array 1
+  FIELDS: array 2
+  a: array 3
+  x: scalar 0
+function f(b, y, z)  # index 0
+  b: array 0
+  y: scalar 0
+  z: scalar 1
+function hi(x)  # index 1
+  x: scalar 0
+`[1:], ""},
 		{[]string{"-d", `$1 { print 1+1 }`}, "", `
 $1 {
     print 1 + 1
diff --git a/interp/interp_test.go b/interp/interp_test.go
index 059c624..54874bb 100644
--- a/interp/interp_test.go
+++ b/interp/interp_test.go
@@ -137,6 +137,8 @@ NR==3, NR==5 { print NR }
 	{`BEGIN { a[1]=1; a[2]=1; a[3]=1; for (k in a) { if (k==2) continue; s++ } print s }`, "", "2\n", "", ""},
 	{`function alen(a, k, n) { n=0; for (k in a) n++; return n }  BEGIN { a[1]=1; a[2]=1; print alen(a) }`, "", "2\n", "", ""},
 	{`BEGIN { a["x"]=1; for (SUBSEP in a) print SUBSEP, a[SUBSEP] }`, "", "x 1\n", "", ""},
+	// TODO: this doesn't work in GoAWK yet
+	// {`BEGIN { a["x"]=1; for (NR in a) print NR, a[NR] }`, "", "x 1\n", "", ""},
 	{`BEGIN { while (i<3) { i++; s++; break } print s }`, "", "1\n", "", ""},
 	{`BEGIN { while (i<3) { i++; if (i==2) continue; s++ } print s }`, "", "2\n", "", ""},
 	{`BEGIN { do { i++; s++; break } while (i<3); print s }`, "", "1\n", "", ""},
@@ -711,12 +713,12 @@ BEGIN { a[1]=2; f1(a); print f2(a, 1) }
 	{`BEGIN { a[x]; a=42 }`, "", "", `parse error at 1:15: can't use array "a" as scalar`, "array"},
 	{`BEGIN { s=42; s[x] }`, "", "", `parse error at 1:15: can't use scalar "s" as array`, "array"},
 	{`function get(a, k) { return a[k] }  BEGIN { a = 42; print get(a, 1); }  # !awk - doesn't error in awk`,
-		"", "", `parse error at 1:59: can't pass scalar "a" as array param`, "attempt to use scalar parameter `a' as an array"},
+		"", "", `parse error at 1:63: can't pass scalar "a" as array param`, "attempt to use scalar parameter `a' as an array"},
 	{`function get(a, k) { return a+k } BEGIN { a[42]; print get(a, 1); }`,
-		"", "", `parse error at 1:56: can't pass array "a" as scalar param`, "array"},
+		"", "", `parse error at 1:60: can't pass array "a" as scalar param`, "array"},
 	{`{ f(z) }  function f(x) { print NR }`, "abc", "1\n", "", ""},
 	{`function f() { f() }  BEGIN { f() }  # !awk !gawk`, "", "", `calling "f" exceeded maximum call depth of 1000`, ""},
-	{`function f(x) { 0 in x }  BEGIN { f(FS) }  # !awk`, "", "", `parse error at 1:35: can't pass scalar "FS" as array param`, "attempt to use scalar parameter `x' as an array"},
+	{`function f(x) { 0 in x }  BEGIN { f(FS) }  # !awk`, "", "", `parse error at 1:37: can't pass scalar "FS" as array param`, "attempt to use scalar parameter `x' as an array"},
 	{`
 function foo(x) { print "foo", x }
 function bar(foo) { print "bar", foo }
@@ -729,8 +731,10 @@ function bar(foo) { print "bar", foo }
 BEGIN { foo(5); bar(10) }
 `, "", "", `parse error at 2:14: can't use function name as parameter name`, "function name"},
 	{`function foo() { print foo }  BEGIN { foo() }`,
-		"", "", `parse error at 1:1: global var "foo" can't also be a function`, "function"},
+		"", "", `parse error at 1:24: global var "foo" can't also be a function`, "function"},
 	{`function f(x) { print x, x(); }  BEGIN { f() }`, "", "", `parse error at 1:26: can't call local variable "x" as function`, "function"},
+	{`function f(x) { print x } BEGIN { print f(1, 2) }  # !gawk`, // only a warning in Gawk
+		"", "", `parse error at 1:41: "f" called with more arguments than declared`, ""},
 
 	// Redirected I/O
 	{`BEGIN { getline x; print x }`, "foo", "foo\n", "", ""},
@@ -1286,17 +1290,22 @@ BEGIN { x=4; y=5; print foo(x), bar(y) }
 				"foo": func(n int) int { return n * n },
 			}},
 		{`BEGIN { a["x"]=1; print foo(a) }`, "", "",
-			`parse error at 1:25: can't pass array "a" to native function`,
+			`parse error at 1:29: can't use array "a" as scalar`,
+			map[string]interface{}{
+				"foo": func(n int) int { return n * n },
+			}},
+		{`BEGIN { print foo(a); a["x"]=1 }`, "", "",
+			`parse error at 1:23: can't use scalar "a" as array`,
 			map[string]interface{}{
 				"foo": func(n int) int { return n * n },
 			}},
 		{`BEGIN { x["x"]=1; print f(x) }  function f(a) { return foo(a) }`, "", "",
-			`parse error at 1:56: can't pass array "a" to native function`,
+			`parse error at 1:27: can't pass array "x" as scalar param`,
 			map[string]interface{}{
 				"foo": func(n int) int { return n * n },
 			}},
 		{`function f(a) { return foo(a) }  BEGIN { x["x"]=1; print f(x) }`, "", "",
-			`parse error at 1:24: can't pass array "a" to native function`,
+			`parse error at 1:60: can't pass array "x" as scalar param`,
 			map[string]interface{}{
 				"foo": func(n int) int { return n * n },
 			}},
diff --git a/parser/parser_test.go b/parser/parser_test.go
index b5ce4e8..1874fbc 100644
--- a/parser/parser_test.go
+++ b/parser/parser_test.go
@@ -39,6 +39,7 @@ $0 {
     print "x" >"file"
     print "x" >>"append"
     print "y" |"prog"
+    delete a
     delete a[k]
     if (c) {
         get(a, k)
diff --git a/testdata/gawk/arrayparm.ok b/testdata/gawk/arrayparm.ok
index da58e1b..b2116c3 100644
--- a/testdata/gawk/arrayparm.ok
+++ b/testdata/gawk/arrayparm.ok
@@ -1,1 +1,1 @@
-parse error at 10:5: can't pass array "foo" as scalar param
\ No newline at end of file
+parse error at 10:10: can't pass array "foo" as scalar param
\ No newline at end of file
diff --git a/testdata/gawk/arryref3.ok b/testdata/gawk/arryref3.ok
index d4af235..e616791 100644
--- a/testdata/gawk/arryref3.ok
+++ b/testdata/gawk/arryref3.ok
@@ -1,1 +1,1 @@
-parse error at 12:2: can't pass array "b" as scalar param
\ No newline at end of file
+parse error at 12:6: can't pass array "b" as scalar param
\ No newline at end of file
diff --git a/testdata/gawk/arryref4.ok b/testdata/gawk/arryref4.ok
index 42e7315..6bd30ec 100644
--- a/testdata/gawk/arryref4.ok
+++ b/testdata/gawk/arryref4.ok
@@ -1,1 +1,1 @@
-parse error at 2:2: can't pass scalar "a" as array param
\ No newline at end of file
+parse error at 2:6: can't pass scalar "a" as array param
\ No newline at end of file
diff --git a/testdata/gawk/arryref5.ok b/testdata/gawk/arryref5.ok
index 42e7315..6bd30ec 100644
--- a/testdata/gawk/arryref5.ok
+++ b/testdata/gawk/arryref5.ok
@@ -1,1 +1,1 @@
-parse error at 2:2: can't pass scalar "a" as array param
\ No newline at end of file
+parse error at 2:6: can't pass scalar "a" as array param
\ No newline at end of file
diff --git a/testdata/gawk/aryprm4.ok b/testdata/gawk/aryprm4.ok
index dbf43b9..6158815 100644
--- a/testdata/gawk/aryprm4.ok
+++ b/testdata/gawk/aryprm4.ok
@@ -1,1 +1,1 @@
-parse error at 5:2: can't pass array "a" as scalar param
\ No newline at end of file
+parse error at 6:2: can't use scalar "a" as array
\ No newline at end of file
diff --git a/testdata/gawk/aryprm6.ok b/testdata/gawk/aryprm6.ok
index 507ffbd..6b49214 100644
--- a/testdata/gawk/aryprm6.ok
+++ b/testdata/gawk/aryprm6.ok
@@ -1,1 +1,1 @@
-parse error at 6:2: can't pass scalar "a" as array param
\ No newline at end of file
+parse error at 6:4: can't pass scalar "a" as array param
\ No newline at end of file
diff --git a/testdata/gawk/aryprm7.ok b/testdata/gawk/aryprm7.ok
index dbf43b9..9c44244 100644
--- a/testdata/gawk/aryprm7.ok
+++ b/testdata/gawk/aryprm7.ok
@@ -1,1 +1,1 @@
-parse error at 5:2: can't pass array "a" as scalar param
\ No newline at end of file
+parse error at 5:7: can't pass scalar "a" as array param
\ No newline at end of file
diff --git a/testdata/gawk/delfunc.ok b/testdata/gawk/delfunc.ok
index b83f623..a7c09a7 100644
--- a/testdata/gawk/delfunc.ok
+++ b/testdata/gawk/delfunc.ok
@@ -1,1 +1,1 @@
-parse error at 1:1: global var "f" can't also be a function
\ No newline at end of file
+parse error at 4:9: global var "f" can't also be a function
\ No newline at end of file
diff --git a/testdata/gawk/fnamedat.ok b/testdata/gawk/fnamedat.ok
index 8449c0a..6b6e4c1 100644
--- a/testdata/gawk/fnamedat.ok
+++ b/testdata/gawk/fnamedat.ok
@@ -1,1 +1,1 @@
-parse error at 1:1: global var "foo" can't also be a function
\ No newline at end of file
+parse error at 1:24: global var "foo" can't also be a function
\ No newline at end of file
diff --git a/testdata/gawk/fnarray.ok b/testdata/gawk/fnarray.ok
index 8449c0a..76ba9e6 100644
--- a/testdata/gawk/fnarray.ok
+++ b/testdata/gawk/fnarray.ok
@@ -1,1 +1,1 @@
-parse error at 1:1: global var "foo" can't also be a function
\ No newline at end of file
+parse error at 5:9: global var "foo" can't also be a function
\ No newline at end of file
diff --git a/testdata/gawk/fnarray2.ok b/testdata/gawk/fnarray2.ok
index cfee62c..5b23cf9 100644
--- a/testdata/gawk/fnarray2.ok
+++ b/testdata/gawk/fnarray2.ok
@@ -1,1 +1,1 @@
-parse error at 1:1: global var "pile" can't also be a function
\ No newline at end of file
+parse error at 3:8: global var "pile" can't also be a function
\ No newline at end of file
diff --git a/testdata/gawk/fnaryscl.ok b/testdata/gawk/fnaryscl.ok
index 5b0d2f4..9a888a7 100644
--- a/testdata/gawk/fnaryscl.ok
+++ b/testdata/gawk/fnaryscl.ok
@@ -1,1 +1,1 @@
-parse error at 3:2: can't pass array "foo" as scalar param
\ No newline at end of file
+parse error at 3:5: can't pass array "foo" as scalar param
\ No newline at end of file
diff --git a/testdata/gawk/fnasgnm.ok b/testdata/gawk/fnasgnm.ok
index 957db6c..94ef211 100644
--- a/testdata/gawk/fnasgnm.ok
+++ b/testdata/gawk/fnasgnm.ok
@@ -1,1 +1,1 @@
-parse error at 1:1: global var "ShowMe" can't also be a function
\ No newline at end of file
+parse error at 14:20: global var "ShowMe" can't also be a function
\ No newline at end of file
diff --git a/testdata/gawk/gsubasgn.ok b/testdata/gawk/gsubasgn.ok
index 264371f..0fc7485 100644
--- a/testdata/gawk/gsubasgn.ok
+++ b/testdata/gawk/gsubasgn.ok
@@ -1,1 +1,1 @@
-parse error at 1:1: global var "test1" can't also be a function
\ No newline at end of file
+parse error at 4:35: global var "test1" can't also be a function
\ No newline at end of file
diff --git a/testdata/gawk/prmarscl.ok b/testdata/gawk/prmarscl.ok
index f7451c7..b733202 100644
--- a/testdata/gawk/prmarscl.ok
+++ b/testdata/gawk/prmarscl.ok
@@ -1,1 +1,1 @@
-parse error at 6:16: can't pass scalar "j" as array param
\ No newline at end of file
+parse error at 6:21: can't pass scalar "j" as array param
\ No newline at end of file