diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-24 10:36:20 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-24 10:36:20 +0300 |
| commit | 73d94aade228aaf22237e99a22fd5a6b943fc21c (patch) | |
| tree | ba3c280fd9a41384bbf5842bb0ceb210e60a5a35 | |
| parent | d1a11d36a25c79384d57e23962f56652bbad1dd1 (diff) | |
rpn: use token-based = detection in handleStandardAssign
Replace fragile strings.Contains checks with proper tokenization.
The old code matched '=' via substring search with hacky guards for
'==' and '!=', which could misparse inputs like 'a == b = 5'.
Now Tokenize(input) splits on whitespace first, so '=' is only
detected as a standalone token — '==' and '!=' are naturally distinct
tokens and can never trigger a false positive.
Also adds regression tests confirming == and != are not treated as
assignments.
| -rw-r--r-- | internal/rpn/assignment_test.go | 53 | ||||
| -rw-r--r-- | internal/rpn/rpn_parse.go | 95 |
2 files changed, 91 insertions, 57 deletions
diff --git a/internal/rpn/assignment_test.go b/internal/rpn/assignment_test.go index 098bef8..1b3cfff 100644 --- a/internal/rpn/assignment_test.go +++ b/internal/rpn/assignment_test.go @@ -273,3 +273,56 @@ func TestAssignmentOperatorRegistry(t *testing.T) { } } } + +// TestAssignmentNotTriggeredByEqualEqual ensures that == is not misparsed as an assignment. +func TestAssignmentNotTriggeredByEqualEqual(t *testing.T) { + vars := NewVariables() + rpn := NewRPN(vars) + + // "a == b" should NOT be treated as an assignment + _, err := rpn.ParseAndEvaluate("a == b") + // It may error (unknown tokens) or produce a result, but it should NOT assign a variable + // The key check: no variable "a" should be created + if _, exists := vars.GetVariable("a"); exists { + t.Error("'a == b' should not create variable a") + } + // The error from parsing == as an unknown operator is acceptable + if err == nil { + // If no error, that's also fine - it just means == was handled somehow + t.Logf("'a == b' returned: %v", err) + } +} + +// TestAssignmentNotTriggeredByNotEqual ensures that != is not misparsed as an assignment. +func TestAssignmentNotTriggeredByNotEqual(t *testing.T) { + vars := NewVariables() + rpn := NewRPN(vars) + + // "a != b" should NOT be treated as an assignment + _, err := rpn.ParseAndEvaluate("a != b") + // The key check: no variable "a" should be created + if _, exists := vars.GetVariable("a"); exists { + t.Error("'a != b' should not create variable a") + } + _ = err // error is acceptable +} + +// TestAssignmentAfterEqualEqual ensures "x 5 =" still works even when == exists in the expression. +func TestAssignmentAfterEqualEqual(t *testing.T) { + vars := NewVariables() + rpn := NewRPN(vars) + + // A standalone assignment should work regardless of == elsewhere + result, err := rpn.ParseAndEvaluate("x 5 =") + if err != nil { + t.Fatalf("'x 5 =' returned error: %v", err) + } + if result != "x = 5" { + t.Errorf("result = %q, want 'x = 5'", result) + } + + val, exists := vars.GetVariable("x") + if !exists || val != 5 { + t.Errorf("x = %v (exists=%v), want 5", val, exists) + } +} diff --git a/internal/rpn/rpn_parse.go b/internal/rpn/rpn_parse.go index 0d96d3f..8775f93 100644 --- a/internal/rpn/rpn_parse.go +++ b/internal/rpn/rpn_parse.go @@ -123,74 +123,55 @@ func handleAssignLeft(input string, r *RPN) (string, bool, error) { // Format: name value = expression (name on bottom, value on top, expression after =) // Or: name = value (single assignment) func handleStandardAssign(input string, r *RPN) (string, bool, error) { - // Check for standard assignment format (name = value or name value = expression) - hasAssignment := strings.Contains(input, " = ") - if !hasAssignment { - // Check for " =" (space before equals) without space after - hasAssignment = strings.Contains(input, " =") - // Additional check: the = must not be followed by another = (i.e., not == or !=) - if hasAssignment && strings.Contains(input, "==") { - hasAssignment = false - } - if hasAssignment && strings.Contains(input, "!=") { - hasAssignment = false + // Tokenize and look for standalone "=" token. + // This avoids false positives from == or != which are separate tokens. + tokens := Tokenize(input) + eqIndex := -1 + for i, tok := range tokens { + if tok == "=" { + eqIndex = i + break } } - - if !hasAssignment { + if eqIndex < 0 { return "", false, nil } - // Handle single assignment: "name = value" - if parts := strings.SplitN(input, " = ", 2); len(parts) == 2 { - name := strings.TrimSpace(parts[0]) - valueStr := strings.TrimSpace(parts[1]) - - // Validate name is a single word (variable name) - nameFields := strings.Fields(name) - if len(nameFields) == 1 { - // Validate value is a single number - valueFields := strings.Fields(valueStr) - if len(valueFields) == 1 { - val, err := strconv.ParseFloat(valueFields[0], 64) - if err != nil { - return "", false, fmt.Errorf("invalid value '%s' for assignment: %w", valueFields[0], err) - } - if err := r.vars.SetVariable(nameFields[0], val); err != nil { - return "", false, err - } - return fmt.Sprintf("%s = %.10g", nameFields[0], val), true, nil - } + // Handle single assignment: "name = value" (3 tokens: name, =, value) + if eqIndex == 1 && len(tokens) == 3 { + name := tokens[0] + valueStr := tokens[2] + val, err := strconv.ParseFloat(valueStr, 64) + if err != nil { + return "", false, nil + } + if err := r.vars.SetVariable(name, val); err != nil { + return "", false, err } + return fmt.Sprintf("%s = %.10g", name, val), true, nil } // Handle assignment with expression: "name value = expression..." - pos := strings.Index(input, " =") - if pos >= 0 { - before := strings.TrimSpace(input[:pos]) - after := strings.TrimSpace(input[pos+2:]) - - beforeFields := strings.Fields(before) - if len(beforeFields) == 2 { - name := beforeFields[0] - valueStr := beforeFields[1] - - // Try to parse value as a number - val, err := strconv.ParseFloat(valueStr, 64) - if err == nil { - // Valid assignment pattern: "name value = expr..." or "name value =" - if err := r.vars.SetVariable(name, val); err != nil { - return "", false, err - } + beforeTokens := tokens[:eqIndex] + afterTokens := tokens[eqIndex+1:] - // If no expression after assignment, just return assignment info - if after == "" { - return fmt.Sprintf("%s = %.10g", name, val), true, nil - } - result, err := r.evaluate(input, strings.Fields(after)) - return result, true, err - } + if len(beforeTokens) == 2 { + name := beforeTokens[0] + valueStr := beforeTokens[1] + + val, err := strconv.ParseFloat(valueStr, 64) + if err != nil { + return "", false, nil + } + if err := r.vars.SetVariable(name, val); err != nil { + return "", false, err + } + + if len(afterTokens) == 0 { + return fmt.Sprintf("%s = %.10g", name, val), true, nil } + result, err := r.evaluate(input, afterTokens) + return result, true, err } return "", false, nil |
