Mako/parser/tests/structs_test.go

640 lines
17 KiB
Go

package parser_test
import (
"testing"
"git.sharkk.net/Sharkk/Mako/parser"
)
func TestBasicStructDefinition(t *testing.T) {
input := `struct Person {
name: string,
age: number
}`
l := parser.NewLexer(input)
p := parser.NewParser(l)
program := p.ParseProgram()
checkParserErrors(t, p)
if len(program.Statements) != 1 {
t.Fatalf("expected 1 statement, got %d", len(program.Statements))
}
stmt, ok := program.Statements[0].(*parser.StructStatement)
if !ok {
t.Fatalf("expected StructStatement, got %T", program.Statements[0])
}
if stmt.Name != "Person" {
t.Errorf("expected struct name 'Person', got %s", stmt.Name)
}
if len(stmt.Fields) != 2 {
t.Fatalf("expected 2 fields, got %d", len(stmt.Fields))
}
// Test first field
if stmt.Fields[0].Name != "name" {
t.Errorf("expected field name 'name', got %s", stmt.Fields[0].Name)
}
if stmt.Fields[0].TypeHint.Type == parser.TypeUnknown {
t.Fatal("expected type hint for name field")
}
if stmt.Fields[0].TypeHint.Type != parser.TypeString {
t.Errorf("expected type string, got %v", stmt.Fields[0].TypeHint.Type)
}
// Test second field
if stmt.Fields[1].Name != "age" {
t.Errorf("expected field name 'age', got %s", stmt.Fields[1].Name)
}
if stmt.Fields[1].TypeHint.Type == parser.TypeUnknown {
t.Fatal("expected type hint for age field")
}
if stmt.Fields[1].TypeHint.Type != parser.TypeNumber {
t.Errorf("expected type number, got %v", stmt.Fields[1].TypeHint.Type)
}
}
func TestEmptyStructDefinition(t *testing.T) {
input := `struct Empty {}`
l := parser.NewLexer(input)
p := parser.NewParser(l)
program := p.ParseProgram()
checkParserErrors(t, p)
if len(program.Statements) != 1 {
t.Fatalf("expected 1 statement, got %d", len(program.Statements))
}
stmt, ok := program.Statements[0].(*parser.StructStatement)
if !ok {
t.Fatalf("expected StructStatement, got %T", program.Statements[0])
}
if stmt.Name != "Empty" {
t.Errorf("expected struct name 'Empty', got %s", stmt.Name)
}
if len(stmt.Fields) != 0 {
t.Errorf("expected 0 fields, got %d", len(stmt.Fields))
}
}
func TestComplexStructDefinition(t *testing.T) {
input := `struct Complex {
id: number,
name: string,
active: bool,
data: table,
callback: function,
optional: any
}`
l := parser.NewLexer(input)
p := parser.NewParser(l)
program := p.ParseProgram()
checkParserErrors(t, p)
if len(program.Statements) != 1 {
t.Fatalf("expected 1 statement, got %d", len(program.Statements))
}
stmt, ok := program.Statements[0].(*parser.StructStatement)
if !ok {
t.Fatalf("expected StructStatement, got %T", program.Statements[0])
}
expectedTypes := []parser.Type{parser.TypeNumber, parser.TypeString, parser.TypeBool, parser.TypeTable, parser.TypeFunction, parser.TypeAny}
expectedNames := []string{"id", "name", "active", "data", "callback", "optional"}
if len(stmt.Fields) != len(expectedTypes) {
t.Fatalf("expected %d fields, got %d", len(expectedTypes), len(stmt.Fields))
}
for i, field := range stmt.Fields {
if field.Name != expectedNames[i] {
t.Errorf("field %d: expected name '%s', got '%s'", i, expectedNames[i], field.Name)
}
if field.TypeHint.Type == parser.TypeUnknown {
t.Fatalf("field %d: expected type hint", i)
}
if field.TypeHint.Type != expectedTypes[i] {
t.Errorf("field %d: expected type %v, got %v", i, expectedTypes[i], field.TypeHint.Type)
}
}
}
func TestMethodDefinition(t *testing.T) {
input := `struct Person {
name: string,
age: number
}
fn Person.getName(): string
return self.name
end
fn Person.setAge(newAge: number)
self.age = newAge
end`
l := parser.NewLexer(input)
p := parser.NewParser(l)
program := p.ParseProgram()
checkParserErrors(t, p)
if len(program.Statements) != 3 {
t.Fatalf("expected 3 statements, got %d", len(program.Statements))
}
// First statement: struct definition
structStmt, ok := program.Statements[0].(*parser.StructStatement)
if !ok {
t.Fatalf("expected StructStatement, got %T", program.Statements[0])
}
if structStmt.Name != "Person" {
t.Errorf("expected struct name 'Person', got %s", structStmt.Name)
}
// Second statement: getter method
method1, ok := program.Statements[1].(*parser.MethodDefinition)
if !ok {
t.Fatalf("expected MethodDefinition, got %T", program.Statements[1])
}
if method1.StructID != structStmt.ID {
t.Errorf("expected struct ID %d, got %d", structStmt.ID, method1.StructID)
}
if method1.MethodName != "getName" {
t.Errorf("expected method name 'getName', got %s", method1.MethodName)
}
if method1.Function.ReturnType.Type == parser.TypeUnknown {
t.Fatal("expected return type for getName method")
}
if method1.Function.ReturnType.Type != parser.TypeString {
t.Errorf("expected return type string, got %v", method1.Function.ReturnType.Type)
}
if len(method1.Function.Parameters) != 0 {
t.Errorf("expected 0 parameters, got %d", len(method1.Function.Parameters))
}
// Third statement: setter method
method2, ok := program.Statements[2].(*parser.MethodDefinition)
if !ok {
t.Fatalf("expected MethodDefinition, got %T", program.Statements[2])
}
if method2.StructID != structStmt.ID {
t.Errorf("expected struct ID %d, got %d", structStmt.ID, method2.StructID)
}
if method2.MethodName != "setAge" {
t.Errorf("expected method name 'setAge', got %s", method2.MethodName)
}
if method2.Function.ReturnType.Type != parser.TypeUnknown {
t.Errorf("expected no return type for setAge method, got %v", method2.Function.ReturnType.Type)
}
if len(method2.Function.Parameters) != 1 {
t.Fatalf("expected 1 parameter, got %d", len(method2.Function.Parameters))
}
if method2.Function.Parameters[0].Name != "newAge" {
t.Errorf("expected parameter name 'newAge', got %s", method2.Function.Parameters[0].Name)
}
if method2.Function.Parameters[0].TypeHint.Type == parser.TypeUnknown {
t.Fatal("expected type hint for newAge parameter")
}
if method2.Function.Parameters[0].TypeHint.Type != parser.TypeNumber {
t.Errorf("expected parameter type number, got %v", method2.Function.Parameters[0].TypeHint.Type)
}
}
func TestStructConstructor(t *testing.T) {
input := `struct Person {
name: string,
age: number
}
person = Person{name = "John", age = 30}
empty = Person{}`
l := parser.NewLexer(input)
p := parser.NewParser(l)
program := p.ParseProgram()
checkParserErrors(t, p)
if len(program.Statements) != 3 {
t.Fatalf("expected 3 statements, got %d", len(program.Statements))
}
structStmt := program.Statements[0].(*parser.StructStatement)
// Second statement: constructor with fields
assign1, ok := program.Statements[1].(*parser.Assignment)
if !ok {
t.Fatalf("expected Assignment, got %T", program.Statements[1])
}
constructor1, ok := assign1.Value.(*parser.StructConstructor)
if !ok {
t.Fatalf("expected StructConstructor, got %T", assign1.Value)
}
if constructor1.StructID != structStmt.ID {
t.Errorf("expected struct ID %d, got %d", structStmt.ID, constructor1.StructID)
}
if len(constructor1.Fields) != 2 {
t.Fatalf("expected 2 fields, got %d", len(constructor1.Fields))
}
// Check name field
nameKey, ok := constructor1.Fields[0].Key.(*parser.Identifier)
if !ok {
t.Fatalf("expected Identifier for name key, got %T", constructor1.Fields[0].Key)
}
if nameKey.Value != "name" {
t.Errorf("expected key 'name', got %s", nameKey.Value)
}
testStringLiteral(t, constructor1.Fields[0].Value, "John")
// Check age field
ageKey, ok := constructor1.Fields[1].Key.(*parser.Identifier)
if !ok {
t.Fatalf("expected Identifier for age key, got %T", constructor1.Fields[1].Key)
}
if ageKey.Value != "age" {
t.Errorf("expected key 'age', got %s", ageKey.Value)
}
testNumberLiteral(t, constructor1.Fields[1].Value, 30)
// Third statement: empty constructor
assign2, ok := program.Statements[2].(*parser.Assignment)
if !ok {
t.Fatalf("expected Assignment, got %T", program.Statements[2])
}
constructor2, ok := assign2.Value.(*parser.StructConstructor)
if !ok {
t.Fatalf("expected StructConstructor, got %T", assign2.Value)
}
if constructor2.StructID != structStmt.ID {
t.Errorf("expected struct ID %d, got %d", structStmt.ID, constructor2.StructID)
}
if len(constructor2.Fields) != 0 {
t.Errorf("expected 0 fields, got %d", len(constructor2.Fields))
}
}
func TestNestedStructTypes(t *testing.T) {
input := `struct Address {
street: string,
city: string
}
struct Person {
name: string,
address: Address
}
person = Person{
name = "John",
address = Address{street = "Main St", city = "NYC"}
}`
l := parser.NewLexer(input)
p := parser.NewParser(l)
program := p.ParseProgram()
checkParserErrors(t, p)
if len(program.Statements) != 3 {
t.Fatalf("expected 3 statements, got %d", len(program.Statements))
}
addressStruct := program.Statements[0].(*parser.StructStatement)
// Check Person struct has Address field type
personStruct, ok := program.Statements[1].(*parser.StructStatement)
if !ok {
t.Fatalf("expected StructStatement, got %T", program.Statements[1])
}
addressField := personStruct.Fields[1]
if addressField.Name != "address" {
t.Errorf("expected field name 'address', got %s", addressField.Name)
}
if addressField.TypeHint.Type != parser.TypeStruct {
t.Errorf("expected field type struct, got %v", addressField.TypeHint.Type)
}
if addressField.TypeHint.StructID != addressStruct.ID {
t.Errorf("expected struct ID %d, got %d", addressStruct.ID, addressField.TypeHint.StructID)
}
// Check nested constructor
assign, ok := program.Statements[2].(*parser.Assignment)
if !ok {
t.Fatalf("expected Assignment, got %T", program.Statements[2])
}
personConstructor, ok := assign.Value.(*parser.StructConstructor)
if !ok {
t.Fatalf("expected StructConstructor, got %T", assign.Value)
}
// Check the nested Address constructor
addressConstructor, ok := personConstructor.Fields[1].Value.(*parser.StructConstructor)
if !ok {
t.Fatalf("expected nested StructConstructor, got %T", personConstructor.Fields[1].Value)
}
if addressConstructor.StructID != addressStruct.ID {
t.Errorf("expected nested struct ID %d, got %d", addressStruct.ID, addressConstructor.StructID)
}
if len(addressConstructor.Fields) != 2 {
t.Errorf("expected 2 fields in nested constructor, got %d", len(addressConstructor.Fields))
}
}
func TestStructIntegrationWithProgram(t *testing.T) {
input := `struct Point {
x: number,
y: number
}
fn Point.distance(other: Point): number
dx = self.x - other.x
dy = self.y - other.y
return (dx * dx + dy * dy)
end
p1 = Point{x = 0, y = 0}
p2 = Point{x = 3, y = 4}
if p1.distance(p2) then
echo "Distance calculated"
end
for i = 1, 10 do
point = Point{x = i, y = i * 2}
echo point.x
end`
l := parser.NewLexer(input)
p := parser.NewParser(l)
program := p.ParseProgram()
checkParserErrors(t, p)
if len(program.Statements) != 6 {
t.Fatalf("expected 6 statements, got %d", len(program.Statements))
}
// Verify struct definition
structStmt, ok := program.Statements[0].(*parser.StructStatement)
if !ok {
t.Fatalf("expected StructStatement, got %T", program.Statements[0])
}
if structStmt.Name != "Point" {
t.Errorf("expected struct name 'Point', got %s", structStmt.Name)
}
// Verify method definition
methodStmt, ok := program.Statements[1].(*parser.MethodDefinition)
if !ok {
t.Fatalf("expected MethodDefinition, got %T", program.Statements[1])
}
if methodStmt.StructID != structStmt.ID {
t.Errorf("expected struct ID %d, got %d", structStmt.ID, methodStmt.StructID)
}
if methodStmt.MethodName != "distance" {
t.Errorf("expected method name 'distance', got %s", methodStmt.MethodName)
}
// Verify constructors
for i := 2; i <= 3; i++ {
assign, ok := program.Statements[i].(*parser.Assignment)
if !ok {
t.Fatalf("statement %d: expected Assignment, got %T", i, program.Statements[i])
}
constructor, ok := assign.Value.(*parser.StructConstructor)
if !ok {
t.Fatalf("statement %d: expected StructConstructor, got %T", i, assign.Value)
}
if constructor.StructID != structStmt.ID {
t.Errorf("statement %d: expected struct ID %d, got %d", i, structStmt.ID, constructor.StructID)
}
}
// Verify if statement with method call
ifStmt, ok := program.Statements[4].(*parser.IfStatement)
if !ok {
t.Fatalf("expected IfStatement, got %T", program.Statements[4])
}
callExpr, ok := ifStmt.Condition.(*parser.CallExpression)
if !ok {
t.Fatalf("expected CallExpression in if condition, got %T", ifStmt.Condition)
}
dotExpr, ok := callExpr.Function.(*parser.DotExpression)
if !ok {
t.Fatalf("expected DotExpression for method call, got %T", callExpr.Function)
}
if dotExpr.Key != "distance" {
t.Errorf("expected method name 'distance', got %s", dotExpr.Key)
}
// Verify for loop with struct creation
forStmt, ok := program.Statements[5].(*parser.ForStatement)
if !ok {
t.Fatalf("expected ForStatement, got %T", program.Statements[5])
}
if len(forStmt.Body) != 2 {
t.Errorf("expected 2 statements in for body, got %d", len(forStmt.Body))
}
// Check struct constructor in loop
loopAssign, ok := forStmt.Body[0].(*parser.Assignment)
if !ok {
t.Fatalf("expected Assignment in loop, got %T", forStmt.Body[0])
}
loopConstructor, ok := loopAssign.Value.(*parser.StructConstructor)
if !ok {
t.Fatalf("expected StructConstructor in loop, got %T", loopAssign.Value)
}
if loopConstructor.StructID != structStmt.ID {
t.Errorf("expected struct ID %d in loop, got %d", structStmt.ID, loopConstructor.StructID)
}
}
func TestStructErrorCases(t *testing.T) {
tests := []struct {
name string
input string
expectedErrorSubstring string
}{
{
name: "missing field type",
input: `struct Person {
name
}`,
expectedErrorSubstring: "struct fields require type annotation",
},
{
name: "missing struct name",
input: `struct {
name: string
}`,
expectedErrorSubstring: "expected struct name",
},
{
name: "missing opening brace",
input: `struct Person
name: string
}`,
expectedErrorSubstring: "expected '{' after struct name",
},
{
name: "missing closing brace",
input: `struct Person {
name: string`,
expectedErrorSubstring: "expected next token to be }, got end of file instead",
},
{
name: "invalid field type",
input: `struct Person {
name: invalidtype
}`,
expectedErrorSubstring: "invalid type name 'invalidtype'",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := parser.NewLexer(tt.input)
p := parser.NewParser(l)
_ = p.ParseProgram()
if !p.HasErrors() {
t.Fatalf("expected parser errors, but got none")
}
errors := p.ErrorStrings()
found := false
for _, err := range errors {
if containsSubstring(err, tt.expectedErrorSubstring) {
found = true
break
}
}
if !found {
t.Errorf("expected error containing '%s', got errors: %v", tt.expectedErrorSubstring, errors)
}
})
}
}
func TestSingleLineStruct(t *testing.T) {
input := `struct Person { name: string, age: number }`
l := parser.NewLexer(input)
p := parser.NewParser(l)
program := p.ParseProgram()
checkParserErrors(t, p)
if len(program.Statements) != 1 {
t.Fatalf("expected 1 statement, got %d", len(program.Statements))
}
stmt, ok := program.Statements[0].(*parser.StructStatement)
if !ok {
t.Fatalf("expected StructStatement, got %T", program.Statements[0])
}
if stmt.Name != "Person" {
t.Errorf("expected struct name 'Person', got %s", stmt.Name)
}
if len(stmt.Fields) != 2 {
t.Fatalf("expected 2 fields, got %d", len(stmt.Fields))
}
if stmt.Fields[0].Name != "name" || stmt.Fields[0].TypeHint.Type != parser.TypeString {
t.Errorf("expected first field 'name: string', got '%s: %v'",
stmt.Fields[0].Name, stmt.Fields[0].TypeHint.Type)
}
if stmt.Fields[1].Name != "age" || stmt.Fields[1].TypeHint.Type != parser.TypeNumber {
t.Errorf("expected second field 'age: number', got '%s: %v'",
stmt.Fields[1].Name, stmt.Fields[1].TypeHint.Type)
}
}
func TestStructString(t *testing.T) {
input := `struct Person {
name: string,
age: number
}`
l := parser.NewLexer(input)
p := parser.NewParser(l)
program := p.ParseProgram()
checkParserErrors(t, p)
stmt := program.Statements[0].(*parser.StructStatement)
str := stmt.String()
expected := "struct Person {\n\tname: string,\n\tage: number\n}"
if str != expected {
t.Errorf("expected string representation:\n%s\ngot:\n%s", expected, str)
}
}
func TestMethodString(t *testing.T) {
input := `struct Person {
name: string
}
fn Person.getName(): string
return self.name
end`
l := parser.NewLexer(input)
p := parser.NewParser(l)
program := p.ParseProgram()
checkParserErrors(t, p)
method := program.Statements[1].(*parser.MethodDefinition)
str := method.String()
if !containsSubstring(str, "fn <struct>.getName") {
t.Errorf("expected method string to contain 'fn <struct>.getName', got: %s", str)
}
if !containsSubstring(str, ": string") {
t.Errorf("expected method string to contain return type, got: %s", str)
}
}
func TestConstructorString(t *testing.T) {
input := `struct Person {
name: string,
age: number
}
person = Person{name = "John", age = 30}`
l := parser.NewLexer(input)
p := parser.NewParser(l)
program := p.ParseProgram()
checkParserErrors(t, p)
assign := program.Statements[1].(*parser.Assignment)
constructor := assign.Value.(*parser.StructConstructor)
str := constructor.String()
expected := `<struct>{name = "John", age = 30.00}`
if str != expected {
t.Errorf("expected constructor string:\n%s\ngot:\n%s", expected, str)
}
}