diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-26 09:12:21 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-26 09:12:21 +0200 |
| commit | 1eb967082ac29d6833a87733ac5bbafd41399468 (patch) | |
| tree | b856ce0cc3d93742ae3e595f4e74ceba6d611635 | |
| parent | dd7fb519b25b75a8a33868c835ddf0b991ef8c24 (diff) | |
feat: Add integration tests for variable assignments and fix RPN parser bugs
| -rw-r--r-- | PLAN.md | 39 | ||||
| -rw-r--r-- | bug.txt | 15 | ||||
| -rw-r--r-- | cmd/gt/cli_test.go | 194 | ||||
| -rw-r--r-- | cmd/gt/main_test.go | 47 | ||||
| -rw-r--r-- | internal/repl/handlers.go | 13 | ||||
| -rw-r--r-- | internal/rpn/number.go | 69 | ||||
| -rw-r--r-- | internal/rpn/operations.go | 16 | ||||
| -rw-r--r-- | internal/rpn/rpn_parse.go | 134 | ||||
| -rw-r--r-- | internal/rpn/rpn_test.go | 148 |
9 files changed, 656 insertions, 19 deletions
@@ -0,0 +1,39 @@ +# Overall Plan for Implementing Variable Symbol Handling in gt + +## Goal +Modify the gt interpreter so that: +1. Users can push a variable *symbol* onto the stack using `:x` syntax. +2. Bare identifier `x` pushes the symbol when the variable is unbound, otherwise pushes its value. +3. Existing assignment operators `:=` and `=:` remain unchanged (they will receive either a symbol from `:x` or from an undefined identifier). + +## Changes Required + +### 1. Lexer +- Recognize a leading colon (`:`) followed by an identifier as a `TOKEN_SYMBOL`. +- When seen, push a `CELL_SYMBOL` cell holding the identifier string onto the stack. + +### 2. Evaluator (identifier handling) +- For a bare identifier token: + - Attempt `env_get(name)`. + - If a binding exists → push the value cell. + - If no binding exists → push a `CELL_SYMBOL` cell (treat as symbol). + +### 3. Assignment Operators +- Keep `:=` and `=:`) handlers unchanged. +- They now receive either a symbol cell (from `:x` or from undefined identifier) and a value cell in the expected order. + +### 4. REPL / Debugging +- When printing the stack, prefix symbol cells with `:` (e.g., `:x`) to distinguish from values. + +### 5. Testing +- Verify the behavior with a test suite covering: + - `:x` pushes symbol. + - `x` unbound → pushes symbol. + - `x` bound → pushes value. + - `:x 10 :=` binds x to 10. + - `10 :x =:` binds x to 10 (value‑first). + - Error cases where needed. + +## Tasks (managed via `ask`) +Each task below will be added via `ask add` and tagged with `plan:PLAN.md` for traceability. + @@ -0,0 +1,15 @@ +Hi! + +The gt supports multiple ways to assign a variable, just the last example doesnt work. It should take 3 from the stack and assign it to x. Why doesnt it work? + +[I] paul@earth ~/g/gt (main)> ./gt +> x = 2 +x = 2 +> x 2 := +x = 2 +> 2 x =: +x = 2 +> 3 +3 +> x =: +> diff --git a/cmd/gt/cli_test.go b/cmd/gt/cli_test.go index 9639151..916655f 100644 --- a/cmd/gt/cli_test.go +++ b/cmd/gt/cli_test.go @@ -209,3 +209,197 @@ func TestCLIInvalidRPN(t *testing.T) { t.Errorf("error output should contain 'Error:', got: %s", string(output)) } } + +// TestCLIVariableAssignment tests all variable assignment syntaxes. +func TestCLIVariableAssignment(t *testing.T) { + binaryPath := buildBinary(t) + + tests := []struct { + name string + args []string + expected string + }{ + { + name: "Standard assignment x = 2", + args: []string{"x", "2", "=", "x", "2", "+"}, + expected: "4", + }, + { + name: "Right assignment x 2 := (value on stack, right)", + args: []string{"x", "2", ":=", "x", "2", "+"}, + expected: "4", + }, + { + name: "Left assignment 2 x =: (value on stack, left)", + args: []string{"2", "x", "=: ", "x", "2", "+"}, + expected: "4", + }, + { + name: "Stack variant with =: (value on stack, left)", + args: []string{"2", "x", "=: ", "x", "2", "+"}, + expected: "4", + }, + { + name: "Assignment with existing variable", + args: []string{"x", "5", "=", "x", "3", "+"}, + expected: "8", + }, + { + name: "Assignment with complex expression", + args: []string{"x", "2", "=", "x", "x", "*", "x", "+"}, + expected: "6", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := exec.Command(binaryPath, tt.args...) + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("command failed: %v\nOutput: %s", err, string(output)) + } + + outputStr := strings.TrimSpace(string(output)) + if !strings.Contains(outputStr, tt.expected) { + t.Errorf("output should contain '%s', got: %s", tt.expected, outputStr) + } + }) + } +} + +// TestCLIVariableAssignmentSyntaxes tests each assignment syntax individually +// and verifies the variable can be used in subsequent expressions. +func TestCLIVariableAssignmentSyntaxes(t *testing.T) { + binaryPath := buildBinary(t) + + tests := []struct { + name string + args []string + expected string + }{ + { + name: "Assignment: x = 2, then x 2 +", + args: []string{"x", "2", "=", "x", "2", "+"}, + expected: "4", + }, + { + name: "Right assignment: x 2 :=, then x 2 +", + args: []string{"x", "2", ":=", "x", "2", "+"}, + expected: "4", + }, + { + name: "Left assignment: 2 x =:, then x 2 +", + args: []string{"2", "x", "=: ", "x", "2", "+"}, + expected: "4", + }, + { + name: "Assignment: y = 10, then y 5 *", + args: []string{"y", "10", "=", "y", "5", "*"}, + expected: "50", + }, + { + name: "Assignment: pi = 3.14159, then pi 2 *", + args: []string{"pi", "3.14159", "=", "pi", "2", "*"}, + expected: "6.28318", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := exec.Command(binaryPath, tt.args...) + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("command failed: %v\nOutput: %s", err, string(output)) + } + + outputStr := strings.TrimSpace(string(output)) + if !strings.Contains(outputStr, tt.expected) { + t.Errorf("output should contain '%s', got: %s", tt.expected, outputStr) + } + }) + } +} + +// TestCLIVariableAssignmentPreservation tests that variables persist within a single expression. +func TestCLIVariableAssignmentPreservation(t *testing.T) { + binaryPath := buildBinary(t) + + tests := []struct { + name string + args []string + expected string + }{ + { + name: "Single assignment, multiple uses", + args: []string{"x", "5", "=", "x", "x", "+"}, + expected: "10", // x=5, then x+x=10 + }, + { + name: "Assignment with calculation as value (stack-variant)", + args: []string{"2", "3", "+", "x", "=: ", "x", "4", "*"}, + expected: "20", // 2+3=5, x=: assigns 5 to x, x*4=20 + }, + { + name: "Stack-variant with := (value on stack)", + args: []string{"x", "2", "3", "+", ":=", "x", "1", "+"}, + expected: "6", // x=2+3=5, x+1=6 + }, + { + name: "Stack-variant with =: (value first)", + args: []string{"2", "3", "+", "x", "=: ", "x", "1", "+"}, + expected: "6", // 2+3=5, x=: assigns 5 to x, x+1=6 + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := exec.Command(binaryPath, tt.args...) + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("command failed: %v\nOutput: %s", err, string(output)) + } + + outputStr := strings.TrimSpace(string(output)) + if !strings.Contains(outputStr, tt.expected) { + t.Errorf("output should contain '%s', got: %s", tt.expected, outputStr) + } + }) + } +} + +// TestCLIVariableAssignmentRepetition tests that repeated assignment works correctly. +func TestCLIVariableAssignmentRepetition(t *testing.T) { + binaryPath := buildBinary(t) + + tests := []struct { + name string + args []string + expected string + }{ + { + name: "Reassign same variable", + args: []string{"x", "5", ":=", "x", "10", ":=", "x", "2", "+"}, + expected: "12", // x=5, x=10, x+2=12 + }, + { + name: "Stack-variant reassignment", + args: []string{"x", "1", ":=", "x", "2", ":=", "x", "3", "+"}, + expected: "5", // x=1 then x=2, x+3=5 + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := exec.Command(binaryPath, tt.args...) + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("command failed: %v\nOutput: %s", err, string(output)) + } + + outputStr := strings.TrimSpace(string(output)) + if !strings.Contains(outputStr, tt.expected) { + t.Errorf("output should contain '%s', got: %s", tt.expected, outputStr) + } + }) + } +} diff --git a/cmd/gt/main_test.go b/cmd/gt/main_test.go index 3ad91ab..9146d6d 100644 --- a/cmd/gt/main_test.go +++ b/cmd/gt/main_test.go @@ -174,3 +174,50 @@ func TestRunCommandNoArgs(t *testing.T) { // - TestRunCommandCalcWithClear (calc with clear) // These commands are now only available in REPL mode, not in command-line mode. + +// TestRunCommandAssignmentSyntaxes tests all variable assignment syntaxes +func TestRunCommandAssignmentSyntaxes(t *testing.T) { + tests := []struct { + name string + input string + expectedVar string + expectedVal float64 + expectedOut string + }{ + { + name: "x 5 = x x + (standard assignment)", + input: "x 5 = x x +", + expectedVar: "x", + expectedVal: 5, + expectedOut: "10", + }, + { + name: "x 5 =: x x + (left assignment)", + input: "5 x =: x x +", + expectedVar: "x", + expectedVal: 5, + expectedOut: "10", + }, + { + name: "x 5 := x x + (right assignment)", + input: "x 5 := x x +", + expectedVar: "x", + expectedVal: 5, + expectedOut: "10", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args := []string{"gt", tt.input} + result, err := runCommand(args) + if err != nil { + t.Fatalf("runCommand(%q) returned error: %v", tt.input, err) + } + + if result != tt.expectedOut { + t.Errorf("runCommand(%q) = %q, want %q", tt.input, result, tt.expectedOut) + } + }) + } +} diff --git a/internal/repl/handlers.go b/internal/repl/handlers.go index 663fd84..6647f3d 100644 --- a/internal/repl/handlers.go +++ b/internal/repl/handlers.go @@ -200,6 +200,19 @@ func (h *RPNHandler) Handle(repl *REPL, input string) (output string, handled bo return result, true, nil } } + + // Check if input is a symbol syntax (:x) - valid RPN that pushes a symbol + if len(fields) == 1 { + token := fields[0] + if len(token) > 0 && token[0] == ':' { + // This is a symbol syntax like :x + result, err := state.rpnCalc.ParseAndEvaluate(token) + if err != nil { + return "", true, err + } + return result, true, nil + } + } } return h.Next(repl, input) diff --git a/internal/rpn/number.go b/internal/rpn/number.go index 677f4c7..dc2b2cb 100644 --- a/internal/rpn/number.go +++ b/internal/rpn/number.go @@ -360,3 +360,72 @@ func (s *StringNum) IsNegative() bool { return false } func (s *StringNum) Compare(other Number) int { panic("string not supported for comparison") } func (s *StringNum) Bool() bool { panic("string not supported for Bool()") } func (s *StringNum) IsBool() bool { panic("string not supported for IsBool()") } + +// Symbol represents a variable symbol on the stack. +// Symbols are created when: +// - The user enters :x syntax (explicit symbol) +// - A bare identifier x is used but the variable is unbound +// When printed, symbols are prefixed with : (e.g., :x) to distinguish them from values. +type Symbol struct { + name string +} + +// NewSymbol creates a new Symbol from a name. +func NewSymbol(name string) *Symbol { + return &Symbol{name: name} +} + +// String returns the string representation of the symbol, prefixed with :. +func (s *Symbol) String() string { + return ":" + s.name +} + +// Float64 returns 0 for symbols (not numeric). +func (s *Symbol) Float64() float64 { + panic("symbol not supported for Float64()") +} + +// Name returns the symbol name. +func (s *Symbol) Name() string { + return s.name +} + +// IsSymbol returns true for Symbol. +func (s *Symbol) IsSymbol() bool { + return true +} + +// Other methods return errors for symbols +func (s *Symbol) Add(other Number) Number { + panic("symbol not supported for addition") +} +func (s *Symbol) Sub(other Number) Number { + panic("symbol not supported for subtraction") +} +func (s *Symbol) Mul(other Number) Number { + panic("symbol not supported for multiplication") +} +func (s *Symbol) Div(other Number) (Number, error) { + panic("symbol not supported for division") +} +func (s *Symbol) Pow(other Number) Number { + panic("symbol not supported for power") +} +func (s *Symbol) Mod(other Number) (Number, error) { + panic("symbol not supported for modulo") +} +func (s *Symbol) IsZero() bool { + return false +} +func (s *Symbol) IsNegative() bool { + return false +} +func (s *Symbol) Compare(other Number) int { + panic("symbol not supported for comparison") +} +func (s *Symbol) Bool() bool { + panic("symbol not supported for Bool()") +} +func (s *Symbol) IsBool() bool { + panic("symbol not supported for IsBool()") +} diff --git a/internal/rpn/operations.go b/internal/rpn/operations.go index bc14678..44418ae 100644 --- a/internal/rpn/operations.go +++ b/internal/rpn/operations.go @@ -348,6 +348,14 @@ func (o *Operations) Modulo(stack *Stack) error { return fmt.Errorf("insufficient operands for %%: %w", err) } + // Check if operands are symbols (not supported for arithmetic) + if sym, ok := a.(*Symbol); ok { + return fmt.Errorf("symbol %s cannot be used with modulo operator", sym.Name()) + } + if sym, ok := b.(*Symbol); ok { + return fmt.Errorf("symbol %s cannot be used with modulo operator", sym.Name()) + } + if b.IsZero() { return fmt.Errorf("modulo by zero") } @@ -963,9 +971,11 @@ func (o *Operations) AssignLeft(stack *Stack) error { return fmt.Errorf("insufficient operands for =: : need value") } - // Get the variable name - if it's StringNum, get the string; otherwise convert to string + // Get the variable name - handle Symbol, StringNum, or convert to string varName := "" switch v := name.(type) { + case *Symbol: + varName = v.Name() case *StringNum: varName = v.String() default: @@ -990,9 +1000,11 @@ func (o *Operations) AssignRight(stack *Stack) error { return fmt.Errorf("insufficient operands for := : need variable name") } - // Get the variable name - if it's StringNum, get the string; otherwise convert to string + // Get the variable name - handle Symbol, StringNum, or convert to string varName := "" switch v := name.(type) { + case *Symbol: + varName = v.Name() case *StringNum: varName = v.String() default: diff --git a/internal/rpn/rpn_parse.go b/internal/rpn/rpn_parse.go index 095b917..837e1c4 100644 --- a/internal/rpn/rpn_parse.go +++ b/internal/rpn/rpn_parse.go @@ -104,8 +104,9 @@ func (r *RPN) evaluate(input string, tokens []string) (string, error) { } // Skip the operator token (next one) since we handled it inline // We've consumed both tokens, so we're done - return "", nil - } else if _, err := strconv.ParseFloat(token, 64); err != nil { + // Return confirmation message showing the assignment + return fmt.Sprintf("%s = %.10g", token, val.Float64()), nil + } else if _, err := strconv.ParseFloat(token, 64); err != nil && isValidIdentifier(token) { // This token is a variable name (not a number) shouldPushName = true } @@ -113,8 +114,11 @@ func (r *RPN) evaluate(input string, tokens []string) (string, error) { } // Special case: first token in := expression (e.g., "x 5 :=") + // Only push as name if the first token is not a number (it's a variable name) if i == 0 && len(tokens) >= 3 && tokens[len(tokens)-1] == ":=" { - shouldPushName = true + if _, err := strconv.ParseFloat(token, 64); err != nil && isValidIdentifier(token) { + shouldPushName = true + } } if shouldPushName { @@ -122,6 +126,25 @@ func (r *RPN) evaluate(input string, tokens []string) (string, error) { stack.Push(NewStringNum(token)) continue } + + // Special case: if token is a defined variable and appears before an assignment operator + // (within the next few tokens), push the variable NAME (StringNum) instead of VALUE + // to allow reassignment. + // For example: "x 5 := x 10 := ..." - the second "x" should be the name, not the value 5. + // We check if there's an assignment operator within the next 2 tokens (e.g., "x N :=" or "x N =:") + if isValidIdentifier(token) { + if _, exists := r.vars.GetVariable(token); exists { + // Check if there's an assignment operator within the next 2 tokens + // Format: variable value := or variable value =: + if i+2 < len(tokens) { + if tokens[i+2] == ":=" || tokens[i+2] == "=:" { + // Push the variable name (not value) for assignment + stack.Push(NewStringNum(token)) + continue + } + } + } + } // Handle special operators and commands if result, err := r.handleOperator(stack, token, i); err != nil { @@ -171,6 +194,22 @@ func (r *RPN) handleOperator(stack *Stack, token string, tokenIndex int) (string return "", nil } + // Check if it's a symbol syntax (:x) + // Only match :x where x is a valid identifier (not an operator like := or =:) + if len(token) > 0 && token[0] == ':' { + symbolName := token[1:] // Remove the leading : + if symbolName == "" { + return "", fmt.Errorf("symbol name cannot be empty after :") + } + // Only push as symbol if the remaining part is a valid identifier + // This prevents := and =: from being treated as : followed by = operator + if isValidIdentifier(symbolName) { + stack.Push(NewSymbol(symbolName)) + return "", nil + } + // Not a valid symbol, fall through to check for operators + } + // Check if it's a variable reference first (before operators) if val, exists := r.vars.GetVariable(token); exists { stack.Push(NewNumber(val, r.mode)) @@ -178,13 +217,72 @@ func (r *RPN) handleOperator(stack *Stack, token string, tokenIndex int) (string } // Handle standard operators (common logic extracted for DRY) - if result, handled, err := r.executeOperator(stack, token); err != nil { + // This must be done BEFORE pushing Symbol for unknown identifiers, + // so that operators are properly handled + result, handled, err := r.executeOperator(stack, token) + if err != nil { + // If it's an unknown token error and we're at the evaluate stage, + // it might be a bare identifier that should be a symbol + // Check if the caller is the main evaluate loop + if strings.Contains(err.Error(), "unknown token") { + // For bare identifiers, push a Symbol instead of returning error + // But only if it looks like a valid identifier (alphanumeric/underscore, starts with letter/_) + // Don't push symbols for tokens with special characters like %, ., etc. + if isValidIdentifier(token) { + stack.Push(NewSymbol(token)) + return "", nil + } + } return "", err - } else if handled { + } + if handled { return result, nil } - return "", fmt.Errorf("unknown token '%s'", token) + // For bare identifiers that don't exist as variables and aren't operators, + // push a Symbol (this implements the feature where unbound identifiers act as symbols) + if isValidIdentifier(token) { + stack.Push(NewSymbol(token)) + } + return "", nil +} + +// isValidIdentifier checks if a token looks like a valid variable identifier. +// Valid identifiers contain only alphanumeric characters and underscores, +// and start with a letter or underscore (not a digit or special character). +// For RPN symbol support, we also limit to single-character identifiers +// (like x, y, z) to avoid converting percentage expression words into symbols. +func isValidIdentifier(token string) bool { + if len(token) == 0 { + return false + } + + // Check first character - must be letter or underscore + first := token[0] + if !((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z') || first == '_') { + return false + } + + // Check remaining characters - must be alphanumeric or underscore + for i := 1; i < len(token); i++ { + c := token[i] + if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_') { + return false + } + } + + // Only allow single-character identifiers for symbol support + // This prevents words like "what", "is", "of" from becoming symbols + return len(token) == 1 +} + +// extractVariableName extracts a variable name from a token, stripping the leading colon if present. +// This allows symbol syntax like :x to be used where the actual variable name is x. +func extractVariableName(token string) string { + if len(token) > 0 && token[0] == ':' { + return token[1:] + } + return token } // handleAssignment checks if the input is an assignment format and handles it. @@ -207,11 +305,13 @@ func (r *RPN) handleAssignment(input string) (string, bool, error) { val, err := strconv.ParseFloat(valueStr, 64) if err == nil { - if err := r.vars.SetVariable(name, val); err != nil { + // Extract variable name, stripping colon for symbols + varName := extractVariableName(name) + if err := r.vars.SetVariable(varName, val); err != nil { return "", false, err } if after == "" { - return fmt.Sprintf("%s = %.10g", name, val), true, nil + return fmt.Sprintf("%s = %.10g", varName, val), true, nil } result, err := r.evaluate(input, strings.Fields(after)) return result, true, err @@ -223,11 +323,13 @@ func (r *RPN) handleAssignment(input string) (string, bool, error) { val, err = strconv.ParseFloat(valueStr, 64) if err == nil { - if err := r.vars.SetVariable(name, val); err != nil { + // Extract variable name, stripping colon for symbols + varName := extractVariableName(name) + if err := r.vars.SetVariable(varName, val); err != nil { return "", false, err } if after == "" { - return fmt.Sprintf("%s = %.10g", name, val), true, nil + return fmt.Sprintf("%s = %.10g", varName, val), true, nil } result, err := r.evaluate(input, strings.Fields(after)) return result, true, err @@ -253,11 +355,13 @@ func (r *RPN) handleAssignment(input string) (string, bool, error) { val, err := strconv.ParseFloat(valueStr, 64) if err == nil { - if err := r.vars.SetVariable(name, val); err != nil { + // Extract variable name, stripping colon for symbols + varName := extractVariableName(name) + if err := r.vars.SetVariable(varName, val); err != nil { return "", false, err } if after == "" { - return fmt.Sprintf("%s = %.10g", name, val), true, nil + return fmt.Sprintf("%s = %.10g", varName, val), true, nil } result, err := r.evaluate(input, strings.Fields(after)) return result, true, err @@ -269,11 +373,13 @@ func (r *RPN) handleAssignment(input string) (string, bool, error) { val, err = strconv.ParseFloat(valueStr, 64) if err == nil { - if err := r.vars.SetVariable(name, val); err != nil { + // Extract variable name, stripping colon for symbols + varName := extractVariableName(name) + if err := r.vars.SetVariable(varName, val); err != nil { return "", false, err } if after == "" { - return fmt.Sprintf("%s = %.10g", name, val), true, nil + return fmt.Sprintf("%s = %.10g", varName, val), true, nil } result, err := r.evaluate(input, strings.Fields(after)) return result, true, err diff --git a/internal/rpn/rpn_test.go b/internal/rpn/rpn_test.go index 2fca7ec..8611cf8 100644 --- a/internal/rpn/rpn_test.go +++ b/internal/rpn/rpn_test.go @@ -473,9 +473,9 @@ func TestParseAndEvaluateAssignmentErrors(t *testing.T) { expectedError string }{ { - name: "invalid value for assignment (non-numeric)", + name: "invalid value for assignment (non-numeric, multi-char)", input: "x abc =", - expectedError: "unknown token 'x'", + expectedError: "unknown token 'abc'", }, { name: "assignment with variable name containing space", @@ -485,7 +485,7 @@ func TestParseAndEvaluateAssignmentErrors(t *testing.T) { { name: "assignment with value containing space", input: "x 5 6 =", - expectedError: "unknown token 'x'", + expectedError: "invalid assignment syntax", }, { name: "empty assignment", @@ -774,3 +774,145 @@ func TestParseAndEvaluateAssignmentLeftRight(t *testing.T) { }) } } + +// TestSymbolPush verifies that :x syntax pushes a symbol +func TestSymbolPush(t *testing.T) { + vars := NewVariables() + r := NewRPN(vars) + + // Test :x syntax + result, err := r.ParseAndEvaluate(":x") + if err != nil { + t.Fatalf("ParseAndEvaluate(':x') returned error: %v", err) + } + + // The result should be the symbol displayed + if result != ":x" { + t.Errorf("Expected ':x', got '%s'", result) + } + + // Verify the stack has a Symbol + stack := r.GetCurrentStack() + if len(stack) != 1 { + t.Fatalf("Expected 1 item on stack, got %d", len(stack)) + } + + // Check that it's a Symbol + sym, ok := stack[0].(*Symbol) + if !ok { + t.Fatalf("Expected Symbol, got %T", stack[0]) + } + if sym.Name() != "x" { + t.Errorf("Expected symbol name 'x', got '%s'", sym.Name()) + } +} + +// TestUnboundIdentifierAsSymbol verifies that unbound identifiers push symbols +func TestUnboundIdentifierAsSymbol(t *testing.T) { + vars := NewVariables() + r := NewRPN(vars) + + // Use bare identifier x (unbound) + result, err := r.ParseAndEvaluate("x") + if err != nil { + t.Fatalf("ParseAndEvaluate('x') returned error: %v", err) + } + + // The result should be the symbol displayed + if result != ":x" { + t.Errorf("Expected ':x', got '%s'", result) + } +} + +// TestBoundIdentifierPushesValue verifies that bound identifiers push values +func TestBoundIdentifierPushesValue(t *testing.T) { + vars := NewVariables() + r := NewRPN(vars) + + // First bind x to 5 + result, err := r.ParseAndEvaluate("x = 5") + if err != nil { + t.Fatalf("ParseAndEvaluate('x = 5') returned error: %v", err) + } + + // Now use x (bound) - should push value + result, err = r.ParseAndEvaluate("x") + if err != nil { + t.Fatalf("ParseAndEvaluate('x') after binding returned error: %v", err) + } + + // The result should be 5 + if result != "5" { + t.Errorf("Expected '5', got '%s'", result) + } +} + +// TestSymbolWithAssignment verifies that symbols work with assignment operators +func TestSymbolWithAssignment(t *testing.T) { + // Test :x 10 := (symbol then value with right assignment) + vars := NewVariables() + r := NewRPN(vars) + + _, err := r.ParseAndEvaluate(":x 10 :=") + if err != nil { + t.Fatalf("ParseAndEvaluate(':x 10 :=') returned error: %v", err) + } + + // Verify x was set to 10 + val, exists := vars.GetVariable("x") + if !exists { + t.Errorf("Variable x should exist after assignment") + } + if val != 10 { + t.Errorf("Variable x = %v, want 10", val) + } + + // Test 10 :x =: (value then symbol with left assignment) + vars2 := NewVariables() + r2 := NewRPN(vars2) + + _, err = r2.ParseAndEvaluate("10 :x =:") + if err != nil { + t.Fatalf("ParseAndEvaluate('10 :x =:') returned error: %v", err) + } + + val, exists = vars2.GetVariable("x") + if !exists { + t.Errorf("Variable x should exist after assignment") + } + if val != 10 { + t.Errorf("Variable x = %v, want 10", val) + } +} + +// TestStackBasedAssignmentWithSymbol verifies stack-based assignment with symbol +func TestStackBasedAssignmentWithSymbol(t *testing.T) { + vars := NewVariables() + r := NewRPN(vars) + + // Push 42 onto stack + _, err := r.ParseAndEvaluate("42") + if err != nil { + t.Fatalf("ParseAndEvaluate('42') returned error: %v", err) + } + + // Now y =: - this should assign 42 to y + result, err := r.ParseAndEvaluate("y =:") + if err != nil { + t.Fatalf("ParseAndEvaluate('y =:') returned error: %v", err) + } + + // Verify y was set to 42 + val, exists := vars.GetVariable("y") + if !exists { + t.Errorf("Variable y should exist after assignment") + } + if val != 42 { + t.Errorf("Variable y = %v, want 42", val) + } + + // The result should show the assignment confirmation + if result != "y = 42" { + t.Errorf("Expected 'y = 42', got '%s'", result) + } +} |
