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 == nil { t.Fatal("expected type hint for name field") } if stmt.Fields[0].TypeHint.Type != "string" { t.Errorf("expected type 'string', got %s", 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 == nil { t.Fatal("expected type hint for age field") } if stmt.Fields[1].TypeHint.Type != "number" { t.Errorf("expected type 'number', got %s", 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 := []string{"number", "string", "bool", "table", "function", "any"} 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 == nil { t.Fatalf("field %d: expected type hint", i) } if field.TypeHint.Type != expectedTypes[i] { t.Errorf("field %d: expected type '%s', got '%s'", 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.StructName != "Person" { t.Errorf("expected struct name 'Person', got %s", method1.StructName) } if method1.MethodName != "getName" { t.Errorf("expected method name 'getName', got %s", method1.MethodName) } if method1.Function.ReturnType == nil { t.Fatal("expected return type for getName method") } if method1.Function.ReturnType.Type != "string" { t.Errorf("expected return type 'string', got %s", 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.StructName != "Person" { t.Errorf("expected struct name 'Person', got %s", method2.StructName) } if method2.MethodName != "setAge" { t.Errorf("expected method name 'setAge', got %s", method2.MethodName) } if method2.Function.ReturnType != nil { t.Errorf("expected no return type for setAge method, got %s", 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 == nil { t.Fatal("expected type hint for newAge parameter") } if method2.Function.Parameters[0].TypeHint.Type != "number" { t.Errorf("expected parameter type 'number', got %s", 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)) } // Second statement: constructor with fields assign1, ok := program.Statements[1].(*parser.AssignStatement) if !ok { t.Fatalf("expected AssignStatement, got %T", program.Statements[1]) } constructor1, ok := assign1.Value.(*parser.StructConstructorExpression) if !ok { t.Fatalf("expected StructConstructorExpression, got %T", assign1.Value) } if constructor1.StructName != "Person" { t.Errorf("expected struct name 'Person', got %s", constructor1.StructName) } 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.AssignStatement) if !ok { t.Fatalf("expected AssignStatement, got %T", program.Statements[2]) } constructor2, ok := assign2.Value.(*parser.StructConstructorExpression) if !ok { t.Fatalf("expected StructConstructorExpression, got %T", assign2.Value) } if constructor2.StructName != "Person" { t.Errorf("expected struct name 'Person', got %s", constructor2.StructName) } 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)) } // 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 != "Address" { t.Errorf("expected field type 'Address', got %s", addressField.TypeHint.Type) } // Check nested constructor assign, ok := program.Statements[2].(*parser.AssignStatement) if !ok { t.Fatalf("expected AssignStatement, got %T", program.Statements[2]) } personConstructor, ok := assign.Value.(*parser.StructConstructorExpression) if !ok { t.Fatalf("expected StructConstructorExpression, got %T", assign.Value) } // Check the nested Address constructor addressConstructor, ok := personConstructor.Fields[1].Value.(*parser.StructConstructorExpression) if !ok { t.Fatalf("expected nested StructConstructorExpression, got %T", personConstructor.Fields[1].Value) } if addressConstructor.StructName != "Address" { t.Errorf("expected nested struct name 'Address', got %s", addressConstructor.StructName) } 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.StructName != "Point" { t.Errorf("expected struct name 'Point', got %s", methodStmt.StructName) } 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.AssignStatement) if !ok { t.Fatalf("statement %d: expected AssignStatement, got %T", i, program.Statements[i]) } constructor, ok := assign.Value.(*parser.StructConstructorExpression) if !ok { t.Fatalf("statement %d: expected StructConstructorExpression, got %T", i, assign.Value) } if constructor.StructName != "Point" { t.Errorf("statement %d: expected struct name 'Point', got %s", i, constructor.StructName) } } // 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.AssignStatement) if !ok { t.Fatalf("expected AssignStatement in loop, got %T", forStmt.Body[0]) } loopConstructor, ok := loopAssign.Value.(*parser.StructConstructorExpression) if !ok { t.Fatalf("expected StructConstructorExpression in loop, got %T", loopAssign.Value) } if loopConstructor.StructName != "Point" { t.Errorf("expected struct name 'Point' in loop, got %s", loopConstructor.StructName) } } 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 != "string" { t.Errorf("expected first field 'name: string', got '%s: %s'", stmt.Fields[0].Name, stmt.Fields[0].TypeHint.Type) } if stmt.Fields[1].Name != "age" || stmt.Fields[1].TypeHint.Type != "number" { t.Errorf("expected second field 'age: number', got '%s: %s'", 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 Person.getName") { t.Errorf("expected method string to contain 'fn Person.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.AssignStatement) constructor := assign.Value.(*parser.StructConstructorExpression) str := constructor.String() expected := `Person{name = "John", age = 30.00}` if str != expected { t.Errorf("expected constructor string:\n%s\ngot:\n%s", expected, str) } }