Mako/parser/tests/scope_test.go

599 lines
16 KiB
Go

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.Assignment); ok {
if assignmentCount >= len(tt.assignments) {
t.Fatalf("more assignments than expected")
}
expected := tt.assignments[assignmentCount]
// Check variable name
ident, ok := assign.Target.(*parser.Identifier)
if !ok {
t.Fatalf("expected Identifier, got %T", assign.Target)
}
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.Target.(*parser.Identifier)
if !ok {
t.Fatalf("assignment %d: expected Identifier, got %T", i, assign.Target)
}
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.Target.(*parser.Identifier)
if !ok {
t.Fatalf("assignment %d: expected Identifier, got %T", i, assign.Target)
}
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.Target.(*parser.Identifier)
if !ok {
t.Fatalf("assignment %d: expected Identifier, got %T", i, assign.Target)
}
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.Target.(*parser.Identifier)
if !ok {
t.Fatalf("assignment %d: expected Identifier, got %T", i, assign.Target)
}
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.Assignment); 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.Target.(*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.Target.(*parser.Identifier)
if !ok {
t.Errorf("assignment %d: expected Identifier, got %T", assignmentCount, assign.Target)
} 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.Target.(*parser.Identifier)
if !ok {
t.Fatalf("assignment %d: expected Identifier, got %T", i, assign.Target)
}
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.Assignment {
var assignments []*parser.Assignment
for _, stmt := range program.Statements {
assignments = append(assignments, extractAssignmentsFromStatement(stmt)...)
}
return assignments
}
func extractAssignmentsFromStatement(stmt parser.Statement) []*parser.Assignment {
var assignments []*parser.Assignment
switch s := stmt.(type) {
case *parser.Assignment:
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
}