assignment implicit scope
This commit is contained in:
parent
fc988d257f
commit
3ea58a55c9
@ -35,13 +35,18 @@ func (p *Program) String() string {
|
||||
|
||||
// AssignStatement represents variable assignment
|
||||
type AssignStatement struct {
|
||||
Name Expression // Changed from *Identifier to Expression for member access
|
||||
Value Expression
|
||||
Name Expression // Changed from *Identifier to Expression for member access
|
||||
Value Expression
|
||||
IsDeclaration bool // true if this is the first assignment in current scope
|
||||
}
|
||||
|
||||
func (as *AssignStatement) statementNode() {}
|
||||
func (as *AssignStatement) String() string {
|
||||
return fmt.Sprintf("%s = %s", as.Name.String(), as.Value.String())
|
||||
prefix := ""
|
||||
if as.IsDeclaration {
|
||||
prefix = "local "
|
||||
}
|
||||
return fmt.Sprintf("%s%s = %s", prefix, as.Name.String(), as.Value.String())
|
||||
}
|
||||
|
||||
// EchoStatement represents echo output statements
|
||||
|
132
parser/parser.go
132
parser/parser.go
@ -30,13 +30,19 @@ type Parser struct {
|
||||
infixParseFns map[TokenType]func(Expression) Expression
|
||||
|
||||
errors []ParseError
|
||||
|
||||
// Scope tracking
|
||||
scopes []map[string]bool // stack of scopes, each tracking declared variables
|
||||
scopeTypes []string // track what type each scope is: "global", "function", "loop"
|
||||
}
|
||||
|
||||
// NewParser creates a new parser instance
|
||||
func NewParser(lexer *Lexer) *Parser {
|
||||
p := &Parser{
|
||||
lexer: lexer,
|
||||
errors: []ParseError{},
|
||||
lexer: lexer,
|
||||
errors: []ParseError{},
|
||||
scopes: []map[string]bool{make(map[string]bool)}, // start with global scope
|
||||
scopeTypes: []string{"global"}, // start with global scope type
|
||||
}
|
||||
|
||||
p.prefixParseFns = make(map[TokenType]func() Expression)
|
||||
@ -73,6 +79,75 @@ func NewParser(lexer *Lexer) *Parser {
|
||||
return p
|
||||
}
|
||||
|
||||
// Scope management
|
||||
func (p *Parser) enterScope(scopeType string) {
|
||||
p.scopes = append(p.scopes, make(map[string]bool))
|
||||
p.scopeTypes = append(p.scopeTypes, scopeType)
|
||||
}
|
||||
|
||||
func (p *Parser) exitScope() {
|
||||
if len(p.scopes) > 1 { // never remove global scope
|
||||
p.scopes = p.scopes[:len(p.scopes)-1]
|
||||
p.scopeTypes = p.scopeTypes[:len(p.scopeTypes)-1]
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Parser) enterFunctionScope() {
|
||||
// Functions create new variable scopes
|
||||
p.enterScope("function")
|
||||
}
|
||||
|
||||
func (p *Parser) exitFunctionScope() {
|
||||
p.exitScope()
|
||||
}
|
||||
|
||||
func (p *Parser) enterLoopScope() {
|
||||
// Create temporary scope for loop variables only
|
||||
p.enterScope("loop")
|
||||
}
|
||||
|
||||
func (p *Parser) exitLoopScope() {
|
||||
// Remove temporary loop scope
|
||||
p.exitScope()
|
||||
}
|
||||
|
||||
func (p *Parser) enterBlockScope() {
|
||||
// Blocks don't create new variable scopes, just control flow scopes
|
||||
// We don't need to track these for variable declarations
|
||||
}
|
||||
|
||||
func (p *Parser) exitBlockScope() {
|
||||
// No-op since blocks don't create variable scopes
|
||||
}
|
||||
|
||||
func (p *Parser) currentVariableScope() map[string]bool {
|
||||
// If we're in a loop scope, declare variables in the parent scope
|
||||
if len(p.scopeTypes) > 1 && p.scopeTypes[len(p.scopeTypes)-1] == "loop" {
|
||||
return p.scopes[len(p.scopes)-2]
|
||||
}
|
||||
// Otherwise use the current scope
|
||||
return p.scopes[len(p.scopes)-1]
|
||||
}
|
||||
|
||||
func (p *Parser) isVariableDeclared(name string) bool {
|
||||
// Check all scopes from current up to global
|
||||
for i := len(p.scopes) - 1; i >= 0; i-- {
|
||||
if p.scopes[i][name] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Parser) declareVariable(name string) {
|
||||
p.currentVariableScope()[name] = true
|
||||
}
|
||||
|
||||
func (p *Parser) declareLoopVariable(name string) {
|
||||
// Loop variables go in the current loop scope
|
||||
p.scopes[len(p.scopes)-1][name] = true
|
||||
}
|
||||
|
||||
// registerPrefix registers a prefix parse function
|
||||
func (p *Parser) registerPrefix(tokenType TokenType, fn func() Expression) {
|
||||
p.prefixParseFns[tokenType] = fn
|
||||
@ -156,10 +231,17 @@ func (p *Parser) parseAssignStatement() *AssignStatement {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate assignment target
|
||||
switch stmt.Name.(type) {
|
||||
case *Identifier, *DotExpression, *IndexExpression:
|
||||
// Valid assignment targets
|
||||
// Validate assignment target and check if it's a declaration
|
||||
switch name := stmt.Name.(type) {
|
||||
case *Identifier:
|
||||
// Simple variable assignment - check if it's a declaration
|
||||
stmt.IsDeclaration = !p.isVariableDeclared(name.Value)
|
||||
if stmt.IsDeclaration {
|
||||
p.declareVariable(name.Value)
|
||||
}
|
||||
case *DotExpression, *IndexExpression:
|
||||
// Member access - never a declaration
|
||||
stmt.IsDeclaration = false
|
||||
default:
|
||||
p.addError("invalid assignment target")
|
||||
return nil
|
||||
@ -264,8 +346,10 @@ func (p *Parser) parseWhileStatement() *WhileStatement {
|
||||
|
||||
p.nextToken() // move past 'do'
|
||||
|
||||
// Parse loop body
|
||||
// Parse loop body (no new variable scope)
|
||||
p.enterBlockScope()
|
||||
stmt.Body = p.parseBlockStatements(END)
|
||||
p.exitBlockScope()
|
||||
|
||||
if !p.curTokenIs(END) {
|
||||
p.addError("expected 'end' to close while loop")
|
||||
@ -349,8 +433,11 @@ func (p *Parser) parseNumericForStatement(variable *Identifier) *ForStatement {
|
||||
|
||||
p.nextToken() // move past 'do'
|
||||
|
||||
// Parse loop body
|
||||
// Create temporary scope for loop variable, assignments in body go to parent scope
|
||||
p.enterLoopScope()
|
||||
p.declareLoopVariable(variable.Value) // loop variable in temporary scope
|
||||
stmt.Body = p.parseBlockStatements(END)
|
||||
p.exitLoopScope() // discard temporary scope with loop variable
|
||||
|
||||
if !p.curTokenIs(END) {
|
||||
p.addError("expected 'end' to close for loop")
|
||||
@ -402,8 +489,14 @@ func (p *Parser) parseForInStatement(firstVar *Identifier) *ForInStatement {
|
||||
|
||||
p.nextToken() // move past 'do'
|
||||
|
||||
// Parse loop body
|
||||
// Create temporary scope for loop variables, assignments in body go to parent scope
|
||||
p.enterLoopScope()
|
||||
if stmt.Key != nil {
|
||||
p.declareLoopVariable(stmt.Key.Value) // loop variable in temporary scope
|
||||
}
|
||||
p.declareLoopVariable(stmt.Value.Value) // loop variable in temporary scope
|
||||
stmt.Body = p.parseBlockStatements(END)
|
||||
p.exitLoopScope() // discard temporary scope with loop variables
|
||||
|
||||
if !p.curTokenIs(END) {
|
||||
p.addError("expected 'end' to close for loop")
|
||||
@ -432,14 +525,16 @@ func (p *Parser) parseIfStatement() *IfStatement {
|
||||
|
||||
p.nextToken() // move past condition (and optional 'then')
|
||||
|
||||
// Check if we immediately hit END (missing body)
|
||||
// Check if we immediately hit END (empty body should be an error)
|
||||
if p.curTokenIs(END) {
|
||||
p.addError("expected 'end' to close if statement")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse if body
|
||||
// Parse if body (no new variable scope)
|
||||
p.enterBlockScope()
|
||||
stmt.Body = p.parseBlockStatements(ELSEIF, ELSE, END)
|
||||
p.exitBlockScope()
|
||||
|
||||
// Parse elseif clauses
|
||||
for p.curTokenIs(ELSEIF) {
|
||||
@ -460,14 +555,22 @@ func (p *Parser) parseIfStatement() *IfStatement {
|
||||
|
||||
p.nextToken() // move past condition (and optional 'then')
|
||||
|
||||
// Parse elseif body (no new variable scope)
|
||||
p.enterBlockScope()
|
||||
elseif.Body = p.parseBlockStatements(ELSEIF, ELSE, END)
|
||||
p.exitBlockScope()
|
||||
|
||||
stmt.ElseIfs = append(stmt.ElseIfs, elseif)
|
||||
}
|
||||
|
||||
// Parse else clause
|
||||
if p.curTokenIs(ELSE) {
|
||||
p.nextToken() // move past 'else'
|
||||
|
||||
// Parse else body (no new variable scope)
|
||||
p.enterBlockScope()
|
||||
stmt.Else = p.parseBlockStatements(END)
|
||||
p.exitBlockScope()
|
||||
}
|
||||
|
||||
if !p.curTokenIs(END) {
|
||||
@ -659,8 +762,13 @@ func (p *Parser) parseFunctionLiteral() Expression {
|
||||
|
||||
p.nextToken() // move past ')'
|
||||
|
||||
// Parse function body
|
||||
// Enter new function scope and declare parameters
|
||||
p.enterFunctionScope()
|
||||
for _, param := range fn.Parameters {
|
||||
p.declareVariable(param)
|
||||
}
|
||||
fn.Body = p.parseBlockStatements(END)
|
||||
p.exitFunctionScope()
|
||||
|
||||
if !p.curTokenIs(END) {
|
||||
p.addError("expected 'end' to close function")
|
||||
|
598
parser/tests/scope_test.go
Normal file
598
parser/tests/scope_test.go
Normal file
@ -0,0 +1,598 @@
|
||||
package parser_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.sharkk.net/Sharkk/Mako/parser"
|
||||
)
|
||||
|
||||
func TestBasicScopeTracking(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
assignments []struct {
|
||||
variable string
|
||||
isDeclaration bool
|
||||
}
|
||||
desc string
|
||||
}{
|
||||
{
|
||||
"x = 5",
|
||||
[]struct {
|
||||
variable string
|
||||
isDeclaration bool
|
||||
}{
|
||||
{"x", true}, // first assignment to x in global scope
|
||||
},
|
||||
"single global declaration",
|
||||
},
|
||||
{
|
||||
`x = 5
|
||||
x = 10`,
|
||||
[]struct {
|
||||
variable string
|
||||
isDeclaration bool
|
||||
}{
|
||||
{"x", true}, // declaration
|
||||
{"x", false}, // assignment to existing
|
||||
},
|
||||
"global declaration then assignment",
|
||||
},
|
||||
{
|
||||
`x = 5
|
||||
y = 10
|
||||
x = 15`,
|
||||
[]struct {
|
||||
variable string
|
||||
isDeclaration bool
|
||||
}{
|
||||
{"x", true}, // declaration
|
||||
{"y", true}, // declaration
|
||||
{"x", false}, // assignment to existing
|
||||
},
|
||||
"multiple variables with reassignment",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
l := parser.NewLexer(tt.input)
|
||||
p := parser.NewParser(l)
|
||||
program := p.ParseProgram()
|
||||
checkParserErrors(t, p)
|
||||
|
||||
assignmentCount := 0
|
||||
for _, stmt := range program.Statements {
|
||||
if assign, ok := stmt.(*parser.AssignStatement); ok {
|
||||
if assignmentCount >= len(tt.assignments) {
|
||||
t.Fatalf("more assignments than expected")
|
||||
}
|
||||
|
||||
expected := tt.assignments[assignmentCount]
|
||||
|
||||
// Check variable name
|
||||
ident, ok := assign.Name.(*parser.Identifier)
|
||||
if !ok {
|
||||
t.Fatalf("expected Identifier, got %T", assign.Name)
|
||||
}
|
||||
|
||||
if ident.Value != expected.variable {
|
||||
t.Errorf("assignment %d: expected variable %s, got %s",
|
||||
assignmentCount, expected.variable, ident.Value)
|
||||
}
|
||||
|
||||
// Check declaration status
|
||||
if assign.IsDeclaration != expected.isDeclaration {
|
||||
t.Errorf("assignment %d (%s): expected IsDeclaration = %t, got %t",
|
||||
assignmentCount, expected.variable, expected.isDeclaration, assign.IsDeclaration)
|
||||
}
|
||||
|
||||
assignmentCount++
|
||||
}
|
||||
}
|
||||
|
||||
if assignmentCount != len(tt.assignments) {
|
||||
t.Errorf("expected %d assignments, found %d", len(tt.assignments), assignmentCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlockScopeTracking(t *testing.T) {
|
||||
input := `x = 5
|
||||
if true then
|
||||
y = 10
|
||||
x = 15
|
||||
z = 20
|
||||
end
|
||||
y = 25
|
||||
z = 30`
|
||||
|
||||
l := parser.NewLexer(input)
|
||||
p := parser.NewParser(l)
|
||||
program := p.ParseProgram()
|
||||
checkParserErrors(t, p)
|
||||
|
||||
// With function/global scoping, all variables are declared at global level
|
||||
expectedAssignments := []struct {
|
||||
variable string
|
||||
isDeclaration bool
|
||||
location string
|
||||
}{
|
||||
{"x", true, "global"}, // x = 5
|
||||
{"y", true, "global"}, // y = 10 (first in global, even though in if block)
|
||||
{"x", false, "global"}, // x = 15 (exists in global)
|
||||
{"z", true, "global"}, // z = 20 (first in global, even though in if block)
|
||||
{"y", false, "global"}, // y = 25 (already declared in global)
|
||||
{"z", false, "global"}, // z = 30 (already declared in global)
|
||||
}
|
||||
|
||||
assignments := extractAssignments(program)
|
||||
|
||||
if len(assignments) != len(expectedAssignments) {
|
||||
t.Fatalf("expected %d assignments, got %d", len(expectedAssignments), len(assignments))
|
||||
}
|
||||
|
||||
for i, expected := range expectedAssignments {
|
||||
assign := assignments[i]
|
||||
ident, ok := assign.Name.(*parser.Identifier)
|
||||
if !ok {
|
||||
t.Fatalf("assignment %d: expected Identifier, got %T", i, assign.Name)
|
||||
}
|
||||
|
||||
if ident.Value != expected.variable {
|
||||
t.Errorf("assignment %d: expected variable %s, got %s",
|
||||
i, expected.variable, ident.Value)
|
||||
}
|
||||
|
||||
if assign.IsDeclaration != expected.isDeclaration {
|
||||
t.Errorf("assignment %d (%s in %s): expected IsDeclaration = %t, got %t",
|
||||
i, expected.variable, expected.location, expected.isDeclaration, assign.IsDeclaration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNestedBlockScopes(t *testing.T) {
|
||||
input := `x = 1
|
||||
if true then
|
||||
y = 2
|
||||
if true then
|
||||
z = 3
|
||||
x = 4
|
||||
y = 5
|
||||
end
|
||||
z = 6
|
||||
end`
|
||||
|
||||
l := parser.NewLexer(input)
|
||||
p := parser.NewParser(l)
|
||||
program := p.ParseProgram()
|
||||
checkParserErrors(t, p)
|
||||
|
||||
// With function/global scoping, all variables are declared at global level
|
||||
expectedAssignments := []struct {
|
||||
variable string
|
||||
isDeclaration bool
|
||||
scope string
|
||||
}{
|
||||
{"x", true, "global"}, // x = 1
|
||||
{"y", true, "global"}, // y = 2 (first in global)
|
||||
{"z", true, "global"}, // z = 3 (first in global)
|
||||
{"x", false, "global"}, // x = 4 (exists in global)
|
||||
{"y", false, "global"}, // y = 5 (exists in global)
|
||||
{"z", false, "global"}, // z = 6 (exists in global)
|
||||
}
|
||||
|
||||
assignments := extractAssignments(program)
|
||||
|
||||
if len(assignments) != len(expectedAssignments) {
|
||||
t.Fatalf("expected %d assignments, got %d", len(expectedAssignments), len(assignments))
|
||||
}
|
||||
|
||||
for i, expected := range expectedAssignments {
|
||||
assign := assignments[i]
|
||||
ident, ok := assign.Name.(*parser.Identifier)
|
||||
if !ok {
|
||||
t.Fatalf("assignment %d: expected Identifier, got %T", i, assign.Name)
|
||||
}
|
||||
|
||||
if ident.Value != expected.variable {
|
||||
t.Errorf("assignment %d: expected variable %s, got %s",
|
||||
i, expected.variable, ident.Value)
|
||||
}
|
||||
|
||||
if assign.IsDeclaration != expected.isDeclaration {
|
||||
t.Errorf("assignment %d (%s in %s): expected IsDeclaration = %t, got %t",
|
||||
i, expected.variable, expected.scope, expected.isDeclaration, assign.IsDeclaration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFunctionScopeTracking(t *testing.T) {
|
||||
input := `x = 1
|
||||
callback = fn(a, b)
|
||||
c = a + b
|
||||
x = 10
|
||||
return c
|
||||
end
|
||||
c = 20`
|
||||
|
||||
l := parser.NewLexer(input)
|
||||
p := parser.NewParser(l)
|
||||
program := p.ParseProgram()
|
||||
checkParserErrors(t, p)
|
||||
|
||||
// Extract assignments from the program
|
||||
assignments := extractAssignments(program)
|
||||
|
||||
expectedAssignments := []struct {
|
||||
variable string
|
||||
isDeclaration bool
|
||||
scope string
|
||||
}{
|
||||
{"x", true, "global"}, // x = 1
|
||||
{"callback", true, "global"}, // callback = fn...
|
||||
{"c", true, "function"}, // c = a + b (first in function scope)
|
||||
{"x", false, "function"}, // x = 10 (exists in global)
|
||||
{"c", true, "global"}, // c = 20 (c was local to function)
|
||||
}
|
||||
|
||||
if len(assignments) != len(expectedAssignments) {
|
||||
t.Fatalf("expected %d assignments, got %d", len(expectedAssignments), len(assignments))
|
||||
}
|
||||
|
||||
for i, expected := range expectedAssignments {
|
||||
assign := assignments[i]
|
||||
ident, ok := assign.Name.(*parser.Identifier)
|
||||
if !ok {
|
||||
t.Fatalf("assignment %d: expected Identifier, got %T", i, assign.Name)
|
||||
}
|
||||
|
||||
if ident.Value != expected.variable {
|
||||
t.Errorf("assignment %d: expected variable %s, got %s",
|
||||
i, expected.variable, ident.Value)
|
||||
}
|
||||
|
||||
if assign.IsDeclaration != expected.isDeclaration {
|
||||
t.Errorf("assignment %d (%s in %s): expected IsDeclaration = %t, got %t",
|
||||
i, expected.variable, expected.scope, expected.isDeclaration, assign.IsDeclaration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoopScopeTracking(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
desc string
|
||||
assignments []struct {
|
||||
variable string
|
||||
isDeclaration bool
|
||||
scope string
|
||||
}
|
||||
}{
|
||||
{
|
||||
`x = 1
|
||||
for i = 1, 10 do
|
||||
y = i
|
||||
x = 5
|
||||
end
|
||||
y = 20`,
|
||||
"numeric for loop",
|
||||
[]struct {
|
||||
variable string
|
||||
isDeclaration bool
|
||||
scope string
|
||||
}{
|
||||
{"x", true, "global"}, // x = 1
|
||||
{"y", true, "global"}, // y = i (first in global, even though in loop)
|
||||
{"x", false, "global"}, // x = 5 (exists in global)
|
||||
{"y", false, "global"}, // y = 20 (already declared in global)
|
||||
},
|
||||
},
|
||||
{
|
||||
`arr = {1, 2, 3}
|
||||
for k, v in arr do
|
||||
sum = sum + v
|
||||
arr = nil
|
||||
end
|
||||
sum = 0`,
|
||||
"for-in loop",
|
||||
[]struct {
|
||||
variable string
|
||||
isDeclaration bool
|
||||
scope string
|
||||
}{
|
||||
{"arr", true, "global"}, // arr = {1, 2, 3}
|
||||
{"sum", true, "global"}, // sum = sum + v (first in global)
|
||||
{"arr", false, "global"}, // arr = nil (exists in global)
|
||||
{"sum", false, "global"}, // sum = 0 (already declared in global)
|
||||
},
|
||||
},
|
||||
{
|
||||
`running = true
|
||||
while running do
|
||||
count = count + 1
|
||||
running = false
|
||||
end
|
||||
count = 0`,
|
||||
"while loop",
|
||||
[]struct {
|
||||
variable string
|
||||
isDeclaration bool
|
||||
scope string
|
||||
}{
|
||||
{"running", true, "global"}, // running = true
|
||||
{"count", true, "global"}, // count = count + 1 (first in global)
|
||||
{"running", false, "global"}, // running = false (exists in global)
|
||||
{"count", false, "global"}, // count = 0 (already declared in global)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
l := parser.NewLexer(tt.input)
|
||||
p := parser.NewParser(l)
|
||||
program := p.ParseProgram()
|
||||
checkParserErrors(t, p)
|
||||
|
||||
assignments := extractAssignments(program)
|
||||
|
||||
if len(assignments) != len(tt.assignments) {
|
||||
t.Fatalf("expected %d assignments, got %d", len(tt.assignments), len(assignments))
|
||||
}
|
||||
|
||||
for i, expected := range tt.assignments {
|
||||
assign := assignments[i]
|
||||
ident, ok := assign.Name.(*parser.Identifier)
|
||||
if !ok {
|
||||
t.Fatalf("assignment %d: expected Identifier, got %T", i, assign.Name)
|
||||
}
|
||||
|
||||
if ident.Value != expected.variable {
|
||||
t.Errorf("assignment %d: expected variable %s, got %s",
|
||||
i, expected.variable, ident.Value)
|
||||
}
|
||||
|
||||
if assign.IsDeclaration != expected.isDeclaration {
|
||||
t.Errorf("assignment %d (%s in %s): expected IsDeclaration = %t, got %t",
|
||||
i, expected.variable, expected.scope, expected.isDeclaration, assign.IsDeclaration)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemberAccessNotDeclaration(t *testing.T) {
|
||||
input := `table = {}
|
||||
table.key = "value"
|
||||
table["index"] = 42
|
||||
arr = {1, 2, 3}
|
||||
arr[1] = 99`
|
||||
|
||||
l := parser.NewLexer(input)
|
||||
p := parser.NewParser(l)
|
||||
program := p.ParseProgram()
|
||||
checkParserErrors(t, p)
|
||||
|
||||
expectedAssignments := []struct {
|
||||
variable string
|
||||
isDeclaration bool
|
||||
isMemberAccess bool
|
||||
}{
|
||||
{"table", true, false}, // table = {} (declaration)
|
||||
{"", false, true}, // table.key = "value" (member access, never declaration)
|
||||
{"", false, true}, // table["index"] = 42 (member access, never declaration)
|
||||
{"arr", true, false}, // arr = {1, 2, 3} (declaration)
|
||||
{"", false, true}, // arr[1] = 99 (member access, never declaration)
|
||||
}
|
||||
|
||||
assignmentCount := 0
|
||||
for _, stmt := range program.Statements {
|
||||
if assign, ok := stmt.(*parser.AssignStatement); ok {
|
||||
if assignmentCount >= len(expectedAssignments) {
|
||||
t.Fatalf("more assignments than expected")
|
||||
}
|
||||
|
||||
expected := expectedAssignments[assignmentCount]
|
||||
|
||||
if expected.isMemberAccess {
|
||||
// Should not be an identifier
|
||||
if _, ok := assign.Name.(*parser.Identifier); ok {
|
||||
t.Errorf("assignment %d: expected member access, got Identifier", assignmentCount)
|
||||
}
|
||||
|
||||
// Member access should never be a declaration
|
||||
if assign.IsDeclaration {
|
||||
t.Errorf("assignment %d: member access should never be declaration", assignmentCount)
|
||||
}
|
||||
} else {
|
||||
// Should be an identifier
|
||||
ident, ok := assign.Name.(*parser.Identifier)
|
||||
if !ok {
|
||||
t.Errorf("assignment %d: expected Identifier, got %T", assignmentCount, assign.Name)
|
||||
} else if ident.Value != expected.variable {
|
||||
t.Errorf("assignment %d: expected variable %s, got %s",
|
||||
assignmentCount, expected.variable, ident.Value)
|
||||
}
|
||||
|
||||
if assign.IsDeclaration != expected.isDeclaration {
|
||||
t.Errorf("assignment %d (%s): expected IsDeclaration = %t, got %t",
|
||||
assignmentCount, expected.variable, expected.isDeclaration, assign.IsDeclaration)
|
||||
}
|
||||
}
|
||||
|
||||
assignmentCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestComplexScopeScenario(t *testing.T) {
|
||||
input := `global_var = "global"
|
||||
counter = 0
|
||||
|
||||
callback = fn(x)
|
||||
local_var = x * 2
|
||||
global_var = "modified"
|
||||
|
||||
if local_var > 10 then
|
||||
temp = "high"
|
||||
counter = counter + 1
|
||||
else
|
||||
temp = "low"
|
||||
counter = counter - 1
|
||||
end
|
||||
|
||||
for i = 1, 3 do
|
||||
temp = temp + i
|
||||
local_var = local_var + i
|
||||
end
|
||||
|
||||
return local_var
|
||||
end
|
||||
|
||||
temp = "global_temp"
|
||||
local_var = "global_local"`
|
||||
|
||||
l := parser.NewLexer(input)
|
||||
p := parser.NewParser(l)
|
||||
program := p.ParseProgram()
|
||||
checkParserErrors(t, p)
|
||||
|
||||
expectedAssignments := []struct {
|
||||
variable string
|
||||
isDeclaration bool
|
||||
scope string
|
||||
}{
|
||||
{"global_var", true, "global"}, // global_var = "global"
|
||||
{"counter", true, "global"}, // counter = 0
|
||||
{"callback", true, "global"}, // callback = fn...
|
||||
{"local_var", true, "function"}, // local_var = x * 2 (first in function)
|
||||
{"global_var", false, "function"}, // global_var = "modified" (exists in global)
|
||||
{"temp", true, "function"}, // temp = "high" (first in function scope)
|
||||
{"counter", false, "function"}, // counter = counter + 1 (exists in global)
|
||||
{"temp", false, "function"}, // temp = "low" (already declared in function)
|
||||
{"counter", false, "function"}, // counter = counter - 1 (exists in global)
|
||||
{"temp", false, "function"}, // temp = temp + i (already declared in function)
|
||||
{"local_var", false, "function"}, // local_var = local_var + i (exists in function)
|
||||
{"temp", true, "global"}, // temp = "global_temp" (temp was local to function)
|
||||
{"local_var", true, "global"}, // local_var = "global_local" (local_var was local to function)
|
||||
}
|
||||
|
||||
assignments := extractAssignments(program)
|
||||
|
||||
if len(assignments) != len(expectedAssignments) {
|
||||
t.Fatalf("expected %d assignments, got %d", len(expectedAssignments), len(assignments))
|
||||
}
|
||||
|
||||
for i, expected := range expectedAssignments {
|
||||
assign := assignments[i]
|
||||
ident, ok := assign.Name.(*parser.Identifier)
|
||||
if !ok {
|
||||
t.Fatalf("assignment %d: expected Identifier, got %T", i, assign.Name)
|
||||
}
|
||||
|
||||
if ident.Value != expected.variable {
|
||||
t.Errorf("assignment %d: expected variable %s, got %s",
|
||||
i, expected.variable, ident.Value)
|
||||
}
|
||||
|
||||
if assign.IsDeclaration != expected.isDeclaration {
|
||||
t.Errorf("assignment %d (%s in %s): expected IsDeclaration = %t, got %t",
|
||||
i, expected.variable, expected.scope, expected.isDeclaration, assign.IsDeclaration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestScopeStringRepresentation(t *testing.T) {
|
||||
input := `x = 5
|
||||
if true then
|
||||
y = 10
|
||||
x = 15
|
||||
end
|
||||
y = 20`
|
||||
|
||||
l := parser.NewLexer(input)
|
||||
p := parser.NewParser(l)
|
||||
program := p.ParseProgram()
|
||||
checkParserErrors(t, p)
|
||||
|
||||
// With function/global scoping:
|
||||
// x = 5: declaration in global
|
||||
// y = 10: declaration in global (first occurrence)
|
||||
// x = 15: assignment in global (already exists)
|
||||
// y = 20: assignment in global (already exists)
|
||||
expected := `local x = 5.00
|
||||
if true then
|
||||
local y = 10.00
|
||||
x = 15.00
|
||||
end
|
||||
y = 20.00`
|
||||
|
||||
result := strings.TrimSpace(program.String())
|
||||
if result != expected {
|
||||
t.Errorf("expected:\n%s\n\ngot:\n%s", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to extract all assignments from a program recursively
|
||||
func extractAssignments(program *parser.Program) []*parser.AssignStatement {
|
||||
var assignments []*parser.AssignStatement
|
||||
|
||||
for _, stmt := range program.Statements {
|
||||
assignments = append(assignments, extractAssignmentsFromStatement(stmt)...)
|
||||
}
|
||||
|
||||
return assignments
|
||||
}
|
||||
|
||||
func extractAssignmentsFromStatement(stmt parser.Statement) []*parser.AssignStatement {
|
||||
var assignments []*parser.AssignStatement
|
||||
|
||||
switch s := stmt.(type) {
|
||||
case *parser.AssignStatement:
|
||||
assignments = append(assignments, s)
|
||||
|
||||
// Check if the value is a function literal with assignments in body
|
||||
if fn, ok := s.Value.(*parser.FunctionLiteral); ok {
|
||||
for _, bodyStmt := range fn.Body {
|
||||
assignments = append(assignments, extractAssignmentsFromStatement(bodyStmt)...)
|
||||
}
|
||||
}
|
||||
|
||||
case *parser.IfStatement:
|
||||
// Extract from if body
|
||||
for _, bodyStmt := range s.Body {
|
||||
assignments = append(assignments, extractAssignmentsFromStatement(bodyStmt)...)
|
||||
}
|
||||
|
||||
// Extract from elseif bodies
|
||||
for _, elseif := range s.ElseIfs {
|
||||
for _, bodyStmt := range elseif.Body {
|
||||
assignments = append(assignments, extractAssignmentsFromStatement(bodyStmt)...)
|
||||
}
|
||||
}
|
||||
|
||||
// Extract from else body
|
||||
for _, bodyStmt := range s.Else {
|
||||
assignments = append(assignments, extractAssignmentsFromStatement(bodyStmt)...)
|
||||
}
|
||||
|
||||
case *parser.WhileStatement:
|
||||
for _, bodyStmt := range s.Body {
|
||||
assignments = append(assignments, extractAssignmentsFromStatement(bodyStmt)...)
|
||||
}
|
||||
|
||||
case *parser.ForStatement:
|
||||
for _, bodyStmt := range s.Body {
|
||||
assignments = append(assignments, extractAssignmentsFromStatement(bodyStmt)...)
|
||||
}
|
||||
|
||||
case *parser.ForInStatement:
|
||||
for _, bodyStmt := range s.Body {
|
||||
assignments = append(assignments, extractAssignmentsFromStatement(bodyStmt)...)
|
||||
}
|
||||
}
|
||||
|
||||
return assignments
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user