Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

shuntingyard_test.gno

9.69 Kb · 307 lines
  1package shuntingyard
  2
  3import (
  4	"strings"
  5	"testing"
  6
  7	"gno.land/p/nt/uassert/v0"
  8)
  9
 10// TestEval pins the grouping rules. Every want below was computed by hand:
 11// a parser checked against another parser only proves the two agree.
 12func TestEval(t *testing.T) {
 13	cases := []struct {
 14		name string
 15		src  string
 16		want int64
 17	}{
 18		{"a single number", "42", 42},
 19		{"multiplication binds tighter", "2+3*4", 14},
 20		{"parentheses beat precedence", "(2+3)*4", 20},
 21		{"subtraction is left associative", "10-3-2", 5},
 22		{"division is left associative", "100/5/2", 10},
 23		{"power is right associative", "2^3^2", 512},
 24		{"unary minus binds looser than power", "-3^2", -9},
 25		{"unary minus over a group", "-(3+4)", -7},
 26		{"two unary minuses cancel", "--5", 5},
 27		{"unary after a binary operator", "3*-2", -6},
 28		{"unary plus is a no-op", "+7-+2", 5},
 29		{"a plus directly after a plus is unary", "1++2", 3},
 30		{"modulo", "17%5", 2},
 31		{"division truncates toward zero", "-7/2", -3},
 32		{"modulo takes the sign of the dividend", "-7%2", -1},
 33		{"whitespace is insignificant", "  1 +\t2 ", 3},
 34		{"zero to the zero is one", "0^0", 1},
 35		{"redundant nesting", "((((1+2))))*3", 9},
 36		{"a long chain", "1+2*3-4/2+5%3", 7},
 37	}
 38
 39	for _, tc := range cases {
 40		t.Run(tc.name, func(t *testing.T) {
 41			got, err := Eval(tc.src)
 42			if err != nil {
 43				t.Errorf("Eval(%q) returned error %v", tc.src, err)
 44				return
 45			}
 46			if got != tc.want {
 47				t.Errorf("Eval(%q) = %d, want %d", tc.src, got, tc.want)
 48			}
 49		})
 50	}
 51}
 52
 53// TestToRPN pins the conversion itself rather than the number that falls out
 54// of it. Eval would pass on a shape that happened to evaluate the same way.
 55func TestToRPN(t *testing.T) {
 56	cases := []struct {
 57		src  string
 58		want string
 59	}{
 60		{"2+3*4", "2 3 4 * +"},
 61		{"(2+3)*4", "2 3 + 4 *"},
 62		{"2^3^2", "2 3 2 ^ ^"},
 63		{"-3^2", "3 2 ^ u-"},
 64		{"3*-2", "3 2 u- *"},
 65		{"10-3-2", "10 3 - 2 -"},
 66		{"-(3+4)", "3 4 + u-"},
 67		{"1+2*3-4/2", "1 2 3 * + 4 2 / -"},
 68	}
 69
 70	for _, tc := range cases {
 71		t.Run(tc.src, func(t *testing.T) {
 72			toks, err := Tokenize(tc.src)
 73			if err != nil {
 74				t.Errorf("Tokenize(%q) returned error %v", tc.src, err)
 75				return
 76			}
 77			rpn, err := ToRPN(toks)
 78			if err != nil {
 79				t.Errorf("ToRPN(%q) returned error %v", tc.src, err)
 80				return
 81			}
 82			if got := FormatRPN(rpn); got != tc.want {
 83				t.Errorf("ToRPN(%q) = %q, want %q", tc.src, got, tc.want)
 84			}
 85		})
 86	}
 87}
 88
 89// TestTokenizeResolvesUnary is the one decision Tokenize makes on its own, and
 90// it is the decision a hand-rolled parser gets wrong: the same byte is two
 91// different operators depending on what precedes it.
 92func TestTokenizeResolvesUnary(t *testing.T) {
 93	cases := []struct {
 94		src    string
 95		at     int // index of the token under test
 96		wantOp string
 97	}{
 98		{"1-2", 1, OpSub},
 99		{"-1", 0, OpNeg},
100		{"(-1)", 1, OpNeg},
101		{"2*-1", 2, OpNeg},
102		{"2^-1", 2, OpNeg},
103		{"+1", 0, OpPos},
104		{"1+2", 1, OpAdd},
105	}
106
107	for _, tc := range cases {
108		t.Run(tc.src, func(t *testing.T) {
109			toks, err := Tokenize(tc.src)
110			if err != nil {
111				t.Errorf("Tokenize(%q) returned error %v", tc.src, err)
112				return
113			}
114			if tc.at >= len(toks) {
115				t.Errorf("Tokenize(%q) produced %d tokens, want more than %d", tc.src, len(toks), tc.at)
116				return
117			}
118			tok := toks[tc.at]
119			if tok.Kind != KindOperator || tok.Op != tc.wantOp {
120				t.Errorf("Tokenize(%q)[%d] = %v, want operator %q", tc.src, tc.at, tok, tc.wantOp)
121			}
122		})
123	}
124}
125
126// TestTokenPositions pins the offsets, which are the only thing making a
127// syntax error actionable to whoever typed the expression.
128func TestTokenPositions(t *testing.T) {
129	toks, err := Tokenize(" 12 + (3)")
130	uassert.NoError(t, err)
131	if len(toks) != 5 {
132		t.Errorf("got %d tokens, want 5", len(toks))
133		return
134	}
135	wantPos := []int{1, 4, 6, 7, 8}
136	for i, want := range wantPos {
137		if toks[i].Pos != want {
138			t.Errorf("token %d (%v) at offset %d, want %d", i, toks[i], toks[i].Pos, want)
139		}
140	}
141}
142
143// TestSyntaxErrors covers every way an expression can be rejected before any
144// arithmetic happens. The message is pinned whole, offset included.
145func TestSyntaxErrors(t *testing.T) {
146	cases := []struct {
147		name string
148		src  string
149		want string
150	}{
151		{"empty", "", "shuntingyard: unexpected end of expression at offset 0"},
152		{"trailing operator", "1+", "shuntingyard: unexpected end of expression at offset 2"},
153		{"two numbers", "1 2", "shuntingyard: unexpected number at offset 2"},
154		{"leading binary operator", "*2", "shuntingyard: unexpected operator \"*\" at offset 0"},
155		{"unclosed group", "(1", "shuntingyard: unbalanced \"(\" at offset 0"},
156		{"unopened group", "1)", "shuntingyard: unbalanced \")\" at offset 1"},
157		{"empty group", "()", "shuntingyard: unexpected \")\" at offset 1"},
158		{"number before group", "2(3)", "shuntingyard: unexpected \"(\" at offset 1"},
159		{"stray character", "1$2", "shuntingyard: unexpected character \"$\" at offset 1"},
160		{"literal out of range", "99999999999999999999", "shuntingyard: integer literal out of range at offset 0"},
161	}
162
163	for _, tc := range cases {
164		t.Run(tc.name, func(t *testing.T) {
165			_, err := Eval(tc.src)
166			if err == nil {
167				t.Errorf("Eval(%q) returned no error, want %q", tc.src, tc.want)
168				return
169			}
170			if got := err.Error(); got != tc.want {
171				t.Errorf("Eval(%q) error = %q, want %q", tc.src, got, tc.want)
172			}
173		})
174	}
175}
176
177// TestEvalErrors is the half that makes the package usable for money: an
178// expression that cannot be evaluated exactly says so instead of wrapping.
179func TestEvalErrors(t *testing.T) {
180	// minInt64 is only reachable through arithmetic: the literal itself is
181	// out of range for the tokenizer, because a number token carries no sign.
182	const mkMin = "((0-9223372036854775807)-1)"
183
184	cases := []struct {
185		name string
186		src  string
187		want error
188	}{
189		{"division by zero", "1/0", ErrDivByZero},
190		{"modulo by zero", "1%0", ErrDivByZero},
191		{"addition overflows", "9223372036854775807+1", ErrOverflow},
192		{"multiplication overflows", "9223372036854775807*2", ErrOverflow},
193		{"subtraction overflows", "(0-9223372036854775807)-2", ErrOverflow},
194		{"power overflows", "2^64", ErrOverflow},
195		{"negating the smallest int64 overflows", "-" + mkMin, ErrOverflow},
196		{"dividing the smallest int64 by -1 overflows", mkMin + "/(0-1)", ErrOverflow},
197		{"negative exponent", "2^-1", ErrNegExponent},
198	}
199
200	for _, tc := range cases {
201		t.Run(tc.name, func(t *testing.T) {
202			got, err := Eval(tc.src)
203			if err != tc.want {
204				t.Errorf("Eval(%q) = %d, %v, want error %v", tc.src, got, err, tc.want)
205			}
206		})
207	}
208}
209
210// TestEvalNearTheLimit is the other side of TestEvalErrors: the checks must
211// not reject an expression that fits exactly.
212func TestEvalNearTheLimit(t *testing.T) {
213	cases := []struct {
214		src  string
215		want int64
216	}{
217		{"9223372036854775806+1", 9223372036854775807},
218		{"4611686018427387903*2", 9223372036854775806},
219		{"2^62", 4611686018427387904},
220		{"(0-9223372036854775807)-0", -9223372036854775807},
221	}
222
223	for _, tc := range cases {
224		t.Run(tc.src, func(t *testing.T) {
225			got, err := Eval(tc.src)
226			if err != nil {
227				t.Errorf("Eval(%q) returned error %v", tc.src, err)
228				return
229			}
230			if got != tc.want {
231				t.Errorf("Eval(%q) = %d, want %d", tc.src, got, tc.want)
232			}
233		})
234	}
235}
236
237// TestEvalRPNRejectsMalformed matters because EvalRPN is exported: a realm may
238// store the RPN and re-run it later, so it has to defend itself rather than
239// trust that ToRPN produced what it is given.
240func TestEvalRPNRejectsMalformed(t *testing.T) {
241	cases := []struct {
242		name string
243		rpn  []Token
244	}{
245		{"empty", nil},
246		{"operator with no operands", []Token{{Kind: KindOperator, Op: OpAdd}}},
247		{"operator with one operand", []Token{{Kind: KindNumber, Num: 2}, {Kind: KindOperator, Op: OpAdd}}},
248		{"two values left over", []Token{{Kind: KindNumber, Num: 2}, {Kind: KindNumber, Num: 3}}},
249		{"unary with no operand", []Token{{Kind: KindOperator, Op: OpNeg}}},
250		{"unknown operator", []Token{{Kind: KindNumber, Num: 2}, {Kind: KindNumber, Num: 3}, {Kind: KindOperator, Op: "&"}}},
251		{"a parenthesis survived the conversion", []Token{{Kind: KindLParen}}},
252	}
253
254	for _, tc := range cases {
255		t.Run(tc.name, func(t *testing.T) {
256			got, err := EvalRPN(tc.rpn)
257			if err != ErrMalformedRPN {
258				t.Errorf("EvalRPN(%v) = %d, %v, want %v", tc.rpn, got, err, ErrMalformedRPN)
259			}
260		})
261	}
262}
263
264// TestTooLong pins the bound. Without it the two stacks are sized by whoever
265// sends the transaction.
266func TestTooLong(t *testing.T) {
267	src := strings.Repeat("1+", MaxTokens) + "1"
268	if _, err := Eval(src); err != ErrTooLong {
269		t.Errorf("Eval of %d tokens returned %v, want %v", 2*MaxTokens+1, err, ErrTooLong)
270	}
271
272	// Just under the bound still parses, so the check is a bound and not an
273	// off-by-one that rejects a long but legal expression.
274	pairs := (MaxTokens - 1) / 2
275	ok := strings.Repeat("1+", pairs) + "1"
276	if _, err := Eval(ok); err != nil {
277		t.Errorf("Eval of %d tokens returned %v, want it to parse", 2*pairs+1, err)
278	}
279}
280
281// TestOperatorsIsACopy: the table is the parser's behaviour, so handing out
282// the backing array would let a caller rendering it change how it parses.
283func TestOperatorsIsACopy(t *testing.T) {
284	got := Operators()
285	if len(got) != len(operators) {
286		t.Errorf("Operators() returned %d rows, want %d", len(got), len(operators))
287		return
288	}
289	got[0].Prec = 99
290	if operators[0].Prec == 99 {
291		t.Errorf("Operators() handed out the table itself")
292	}
293}
294
295// TestFormatRPNEmpty pins the only edge case the formatter has.
296func TestFormatRPNEmpty(t *testing.T) {
297	uassert.Equal(t, "", FormatRPN(nil))
298}
299
300// TestTokenString covers the four kinds, since FormatRPN and the demo both
301// render through it.
302func TestTokenString(t *testing.T) {
303	uassert.Equal(t, "-7", Token{Kind: KindNumber, Num: -7}.String())
304	uassert.Equal(t, "u-", Token{Kind: KindOperator, Op: OpNeg}.String())
305	uassert.Equal(t, "(", Token{Kind: KindLParen}.String())
306	uassert.Equal(t, ")", Token{Kind: KindRParen}.String())
307}