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 /internal/rpn/rpn_parse.go | |
| 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.
Diffstat (limited to 'internal/rpn/rpn_parse.go')
| -rw-r--r-- | internal/rpn/rpn_parse.go | 95 |
1 files changed, 38 insertions, 57 deletions
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 |
