summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-26 09:12:21 +0200
committerPaul Buetow <paul@buetow.org>2026-03-26 09:12:21 +0200
commit1eb967082ac29d6833a87733ac5bbafd41399468 (patch)
treeb856ce0cc3d93742ae3e595f4e74ceba6d611635
parentdd7fb519b25b75a8a33868c835ddf0b991ef8c24 (diff)
feat: Add integration tests for variable assignments and fix RPN parser bugs
-rw-r--r--PLAN.md39
-rw-r--r--bug.txt15
-rw-r--r--cmd/gt/cli_test.go194
-rw-r--r--cmd/gt/main_test.go47
-rw-r--r--internal/repl/handlers.go13
-rw-r--r--internal/rpn/number.go69
-rw-r--r--internal/rpn/operations.go16
-rw-r--r--internal/rpn/rpn_parse.go134
-rw-r--r--internal/rpn/rpn_test.go148
9 files changed, 656 insertions, 19 deletions
diff --git a/PLAN.md b/PLAN.md
new file mode 100644
index 0000000..798bcb2
--- /dev/null
+++ b/PLAN.md
@@ -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.
+
diff --git a/bug.txt b/bug.txt
new file mode 100644
index 0000000..48811dd
--- /dev/null
+++ b/bug.txt
@@ -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)
+ }
+}