Skip to content

Commit fcc9d89

Browse files
committed
make stack have names, improve error message
1 parent 459e315 commit fcc9d89

File tree

3 files changed

+39
-20
lines changed

3 files changed

+39
-20
lines changed

modules/templates/eval/eval.go

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ type Num struct {
1616
}
1717

1818
var opPrecedence = map[string]int{
19-
"(": 1, ")": 1,
19+
// "(": 1, this is for low precedence like function calls, they are handled separately
2020
"or": 2,
2121
"and": 3,
2222
"not": 4,
@@ -26,6 +26,7 @@ var opPrecedence = map[string]int{
2626
}
2727

2828
type stack[T any] struct {
29+
name string
2930
elems []T
3031
}
3132

@@ -35,7 +36,7 @@ func (s *stack[T]) push(t T) {
3536

3637
func (s *stack[T]) pop() T {
3738
if len(s.elems) == 0 {
38-
panic("stack is empty")
39+
panic(s.name + " stack is empty")
3940
}
4041
t := s.elems[len(s.elems)-1]
4142
s.elems = s.elems[:len(s.elems)-1]
@@ -44,19 +45,26 @@ func (s *stack[T]) pop() T {
4445

4546
func (s *stack[T]) peek() T {
4647
if len(s.elems) == 0 {
47-
panic("stack is empty")
48+
panic(s.name + " stack is empty")
4849
}
4950
return s.elems[len(s.elems)-1]
5051
}
5152

5253
type operator string
5354

54-
type Eval struct {
55+
type eval struct {
5556
stackNum stack[Num]
5657
stackOp stack[operator]
5758
funcMap map[string]func([]Num) Num
5859
}
5960

61+
func newEval() *eval {
62+
e := &eval{}
63+
e.stackNum.name = "num"
64+
e.stackOp.name = "op"
65+
return e
66+
}
67+
6068
func toNum(v any) (Num, error) {
6169
switch v := v.(type) {
6270
case string:
@@ -144,7 +152,7 @@ func toOp(v any) (operator, error) {
144152
if v, ok := v.(string); ok {
145153
return operator(v), nil
146154
}
147-
return "", fmt.Errorf("unable to convert %q to operator", v)
155+
return "", fmt.Errorf(`unsupported token type "%T"`, v)
148156
}
149157

150158
func (op operator) hasOpenBracket() bool {
@@ -184,12 +192,14 @@ func (err ExprError) Unwrap() error {
184192
return err.err
185193
}
186194

187-
func (e *Eval) applyOp() {
195+
func (e *eval) applyOp() {
188196
op := e.stackOp.pop()
189197
if op == "not" {
190198
num := e.stackNum.pop()
191199
i, _ := util.ToInt64(num.Value)
192200
e.stackNum.push(Num{truth(i == 0)})
201+
} else if op.hasOpenBracket() || op.isCloseBracket() || op.isComma() {
202+
panic(fmt.Sprintf("incomplete sub-expression with operator %q", op))
193203
} else {
194204
num2 := e.stackNum.pop()
195205
num1 := e.stackNum.pop()
@@ -201,7 +211,7 @@ func (e *Eval) applyOp() {
201211
// If no error occurs, the result is either an int64 or a float64.
202212
// If all numbers are integer, the result is an int64, otherwise if there is any float number, the result is a float64.
203213
// Golang's template syntax supports comparable int types: {{if lt $i32 $i64}} is right.
204-
func (e *Eval) Exec(tokens ...any) (ret Num, err error) {
214+
func (e *eval) Exec(tokens ...any) (ret Num, err error) {
205215
defer func() {
206216
if r := recover(); r != nil {
207217
rErr, ok := r.(error)
@@ -228,7 +238,7 @@ func (e *Eval) Exec(tokens ...any) (ret Num, err error) {
228238
e.stackOp.push(op)
229239
case op.isCloseBracket(), op.isComma():
230240
var stackTopOp operator
231-
for {
241+
for len(e.stackOp.elems) > 0 {
232242
stackTopOp = e.stackOp.peek()
233243
if stackTopOp.hasOpenBracket() || stackTopOp.isComma() {
234244
break
@@ -265,19 +275,19 @@ func (e *Eval) Exec(tokens ...any) (ret Num, err error) {
265275
default:
266276
for len(e.stackOp.elems) > 0 && len(e.stackNum.elems) > 0 {
267277
stackTopOp := e.stackOp.peek()
268-
if stackTopOp.isComma() || precedence(stackTopOp, op) < 0 {
278+
if stackTopOp.hasOpenBracket() || stackTopOp.isComma() || precedence(stackTopOp, op) < 0 {
269279
break
270280
}
271281
e.applyOp()
272282
}
273283
e.stackOp.push(op)
274284
}
275285
}
276-
for len(e.stackOp.elems) > 0 {
286+
for len(e.stackOp.elems) > 0 && !e.stackOp.peek().isComma() {
277287
e.applyOp()
278288
}
279289
if len(e.stackNum.elems) != 1 {
280-
return Num{}, ExprError{"too many final values", tokens, nil}
290+
return Num{}, ExprError{fmt.Sprintf("expect 1 value as final result, but there are %d", len(e.stackNum.elems)), tokens, nil}
281291
}
282292
return e.stackNum.pop(), nil
283293
}
@@ -327,6 +337,7 @@ func fnSum(nums []Num) Num {
327337
}
328338

329339
func Expr(tokens ...any) (Num, error) {
330-
e := &Eval{funcMap: map[string]func([]Num) Num{"sum": fnSum}}
340+
e := newEval()
341+
e.funcMap = map[string]func([]Num) Num{"sum": fnSum}
331342
return e.Exec(tokens...)
332343
}

modules/templates/eval/eval_test.go

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ func TestEval(t *testing.T) {
2323
assert.NoError(t, err)
2424
assert.True(t, math.IsNaN(n.Value.(float64)))
2525

26+
_, err = Expr(nil)
27+
assert.ErrorContains(t, err, "unsupported token type")
28+
_, err = Expr([]string{})
29+
assert.ErrorContains(t, err, "unsupported token type")
30+
_, err = Expr(struct{}{})
31+
assert.ErrorContains(t, err, "unsupported token type")
32+
2633
cases := []struct {
2734
expr string
2835
want any
@@ -72,16 +79,16 @@ func TestEval(t *testing.T) {
7279
errMsg string
7380
}{
7481
{"0 / 0", "integer divide by zero"},
75-
{"1 +", "stack is empty"},
76-
{"+ 1", "stack is empty"},
77-
{"( 1", "stack is empty"},
78-
{"1 )", "stack is empty"},
82+
{"1 +", "num stack is empty"},
83+
{"+ 1", "num stack is empty"},
84+
{"( 1", "incomplete sub-expression"},
85+
{"1 )", "op stack is empty"}, // can not find the corresponding open bracket after the stack becomes empty
86+
{"1 , 2", "expect 1 value as final result"},
87+
{"( 1 , 2 )", "too many values in one bracket"},
7988
{"1 a 2", "unknown operator"},
8089
}
8190
for _, c := range bads {
82-
_, err := Expr(tokens(c.expr)...)
83-
if assert.Error(t, err, "expr: %s", c.expr) {
84-
assert.Contains(t, err.Error(), c.errMsg)
85-
}
91+
_, err = Expr(tokens(c.expr)...)
92+
assert.ErrorContains(t, err, c.errMsg, "expr: %s", c.expr)
8693
}
8794
}

modules/util/util.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"bytes"
88
"crypto/rand"
99
"errors"
10+
"fmt"
1011
"math/big"
1112
"strconv"
1213
"strings"

0 commit comments

Comments
 (0)