diff --git a/parser/ast.go b/parser/ast.go index 1f21c6c..941eb82 100644 --- a/parser/ast.go +++ b/parser/ast.go @@ -66,6 +66,21 @@ func (as *AssignStatement) String() string { return fmt.Sprintf("%s%s = %s", prefix, nameStr, as.Value.String()) } +// AssignExpression represents assignment as an expression (only in parentheses) +type AssignExpression struct { + Name Expression // Target (identifier, dot, or index expression) + Value Expression // Value to assign + IsDeclaration bool // true if this declares a new variable + typeInfo *TypeInfo // type of the expression (same as assigned value) +} + +func (ae *AssignExpression) expressionNode() {} +func (ae *AssignExpression) String() string { + return fmt.Sprintf("(%s = %s)", ae.Name.String(), ae.Value.String()) +} +func (ae *AssignExpression) GetType() *TypeInfo { return ae.typeInfo } +func (ae *AssignExpression) SetType(t *TypeInfo) { ae.typeInfo = t } + // ExpressionStatement represents expressions used as statements type ExpressionStatement struct { Expression Expression diff --git a/parser/parser.go b/parser/parser.go index 989e134..906e559 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -739,9 +739,16 @@ func (p *Parser) parsePrefixExpression() Expression { return expression } +// parseGroupedExpression handles parentheses and assignment expressions func (p *Parser) parseGroupedExpression() Expression { p.nextToken() + // Check if this is an assignment expression inside parentheses + if p.curTokenIs(IDENT) && p.peekTokenIs(ASSIGN) { + return p.parseParenthesizedAssignment() + } + + // Regular grouped expression exp := p.ParseExpression(LOWEST) if exp == nil { return nil @@ -754,6 +761,46 @@ func (p *Parser) parseGroupedExpression() Expression { return exp } +// parseParenthesizedAssignment parses assignment expressions in parentheses +func (p *Parser) parseParenthesizedAssignment() Expression { + // We're at identifier, peek is ASSIGN + target := p.parseIdentifier() + + if !p.expectPeek(ASSIGN) { + return nil + } + + p.nextToken() // move past = + + value := p.ParseExpression(LOWEST) + if value == nil { + p.addError("expected expression after assignment operator") + return nil + } + + if !p.expectPeek(RPAREN) { + return nil + } + + // Create assignment expression + assignExpr := &AssignExpression{ + Name: target, + Value: value, + } + + // Handle variable declaration for assignment expressions + if ident, ok := target.(*Identifier); ok { + assignExpr.IsDeclaration = !p.isVariableDeclared(ident.Value) + if assignExpr.IsDeclaration { + p.declareVariable(ident.Value) + } + } + + // Assignment expression evaluates to the assigned value + assignExpr.SetType(value.GetType()) + return assignExpr +} + func (p *Parser) parseFunctionLiteral() Expression { fn := &FunctionLiteral{} diff --git a/parser/tests/expressions_test.go b/parser/tests/expressions_test.go index 7fd300f..688738e 100644 --- a/parser/tests/expressions_test.go +++ b/parser/tests/expressions_test.go @@ -328,3 +328,149 @@ func TestComplexExpressionsWithComparisons(t *testing.T) { }) } } + +func TestAssignmentExpressions(t *testing.T) { + tests := []struct { + input string + targetName string + value any + isDeclaration bool + desc string + }{ + {"(x = 5)", "x", 5.0, true, "simple assignment"}, + {"(y = true)", "y", true, true, "boolean assignment"}, + {"(name = \"test\")", "name", "test", true, "string assignment"}, + {"(count = nil)", "count", nil, true, "nil assignment"}, + {"(result = x + 1)", "result", "(x + 1.00)", true, "expression assignment"}, + {"(flag = not active)", "flag", "(not active)", true, "prefix expression assignment"}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + l := parser.NewLexer(tt.input) + p := parser.NewParser(l) + expr := p.ParseExpression(parser.LOWEST) + checkParserErrors(t, p) + + assignExpr, ok := expr.(*parser.AssignExpression) + if !ok { + t.Fatalf("expected AssignExpression, got %T", expr) + } + + // Test target name + ident, ok := assignExpr.Name.(*parser.Identifier) + if !ok { + t.Fatalf("expected Identifier for assignment target, got %T", assignExpr.Name) + } + + if ident.Value != tt.targetName { + t.Errorf("expected target name %s, got %s", tt.targetName, ident.Value) + } + + // Test assignment value + switch expected := tt.value.(type) { + case float64: + testNumberLiteral(t, assignExpr.Value, expected) + case string: + if expected == "test" { + testStringLiteral(t, assignExpr.Value, expected) + } else { + // It's an expression string + if assignExpr.Value.String() != expected { + t.Errorf("expected value %s, got %s", expected, assignExpr.Value.String()) + } + } + case bool: + testBooleanLiteral(t, assignExpr.Value, expected) + case nil: + testNilLiteral(t, assignExpr.Value) + } + + // Test declaration flag + if assignExpr.IsDeclaration != tt.isDeclaration { + t.Errorf("expected IsDeclaration %v, got %v", tt.isDeclaration, assignExpr.IsDeclaration) + } + }) + } +} + +func TestAssignmentExpressionWithComplexExpressions(t *testing.T) { + tests := []struct { + input string + desc string + }{ + {"(result = func(a, b))", "function call assignment"}, + {"(value = table[key])", "index expression assignment"}, + {"(prop = obj.field)", "dot expression assignment"}, + {"(sum = a + b * c)", "complex arithmetic assignment"}, + {"(valid = x > 0 and y < 10)", "logical expression assignment"}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + l := parser.NewLexer(tt.input) + p := parser.NewParser(l) + expr := p.ParseExpression(parser.LOWEST) + checkParserErrors(t, p) + + assignExpr, ok := expr.(*parser.AssignExpression) + if !ok { + t.Fatalf("expected AssignExpression, got %T", expr) + } + + if assignExpr.Name == nil { + t.Error("expected non-nil assignment target") + } + + if assignExpr.Value == nil { + t.Error("expected non-nil assignment value") + } + + // Verify string representation + result := assignExpr.String() + if result == "" { + t.Error("expected non-empty string representation") + } + }) + } +} + +func TestAssignmentExpressionErrors(t *testing.T) { + tests := []struct { + input string + expectedErr string + desc string + }{ + {"(x =)", "expected expression after assignment operator", "missing value"}, + {"(= 5)", "unexpected assignment operator", "missing target"}, + {"(x = )", "expected expression after assignment operator", "empty value"}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + l := parser.NewLexer(tt.input) + p := parser.NewParser(l) + expr := p.ParseExpression(parser.LOWEST) + + if p.HasErrors() { + errors := p.ErrorStrings() + found := false + for _, err := range errors { + if containsSubstring(err, tt.expectedErr) { + found = true + break + } + } + if !found { + t.Errorf("expected error containing '%s', got %v", tt.expectedErr, errors) + } + } else { + t.Errorf("expected parse error for input '%s'", tt.input) + } + + if expr != nil { + t.Errorf("expected nil expression for invalid input, got %T", expr) + } + }) + } +} diff --git a/parser/tests/helpers_test.go b/parser/tests/helpers_test.go index fac289f..1cc9430 100644 --- a/parser/tests/helpers_test.go +++ b/parser/tests/helpers_test.go @@ -102,3 +102,17 @@ func checkParserErrors(t *testing.T, p *parser.Parser) { } t.FailNow() } + +func containsSubstring(s, substr string) bool { + return len(s) >= len(substr) && s[:len(substr)] == substr || + len(s) > len(substr) && findSubstring(s, substr) +} + +func findSubstring(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +}