summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-24 10:36:20 +0300
committerPaul Buetow <paul@buetow.org>2026-05-24 10:36:20 +0300
commit73d94aade228aaf22237e99a22fd5a6b943fc21c (patch)
treeba3c280fd9a41384bbf5842bb0ceb210e60a5a35
parentd1a11d36a25c79384d57e23962f56652bbad1dd1 (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.go53
-rw-r--r--internal/rpn/rpn_parse.go95
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