599 lines
16 KiB
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
|
|
}
|