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.gno

14.30 Kb · 541 lines
  1// Package shuntingyard turns an infix arithmetic expression into reverse
  2// Polish notation and evaluates it over int64, with Dijkstra's shunting-yard
  3// algorithm (1961).
  4//
  5// A realm that wants to take a formula from a caller has two bad options and
  6// this one. Hard-coding the arithmetic means a new package path for every new
  7// formula, and a public path can never be redeployed. Accepting a pre-built
  8// RPN string pushes the parsing onto whoever is sending the transaction, who
  9// then has to be trusted to have parsed it the same way. Parsing infix on
 10// chain keeps the formula a piece of data and the meaning a piece of code.
 11//
 12// Three properties make it usable for that rather than only interesting:
 13//
 14//   - Every arithmetic result is checked. An expression that would wrap int64
 15//     returns [ErrOverflow] instead of a plausible wrong number, which matters
 16//     because the usual reason to evaluate a caller's formula is to compute an
 17//     amount.
 18//   - Nothing panics. Every refusal is an error value, so a Render that
 19//     evaluates a stored expression cannot be made permanently unreadable by
 20//     one bad input.
 21//   - Parsing and evaluation are separate. [ToRPN] answers whether an
 22//     expression is well formed, and the RPN it returns is a bounded program a
 23//     realm can store and re-run without parsing it again.
 24//
 25// Integers only, deliberately: see the package README.
 26//
 27// A live demo of this package is at
 28// [r/moul/x/daily/shuntingyarddemo](/r/moul/x/daily/shuntingyarddemo/v0).
 29package shuntingyard
 30
 31import (
 32	"errors"
 33	"math"
 34	"strconv"
 35	"strings"
 36)
 37
 38// MaxTokens bounds how long an expression may be, so a caller cannot size the
 39// two stacks from unbounded input and make the allocation itself the attack.
 40// It is a token count, not a byte count: whitespace and long literals are free.
 41const MaxTokens = 1024
 42
 43// Operator spellings, as they appear in a [Token]. The two unary forms are
 44// separate operators rather than a flag, because a prefix minus and a binary
 45// minus have different arities and different precedence, and conflating them
 46// is the classic way a shunting-yard implementation evaluates -3^2 as 9.
 47const (
 48	OpAdd = "+"
 49	OpSub = "-"
 50	OpMul = "*"
 51	OpDiv = "/"
 52	OpMod = "%"
 53	OpPow = "^"
 54	OpNeg = "u-"
 55	OpPos = "u+"
 56)
 57
 58// Evaluation failures. Each one is a value rather than a panic: see the
 59// package doc.
 60var (
 61	ErrDivByZero    = errors.New("shuntingyard: division by zero")
 62	ErrOverflow     = errors.New("shuntingyard: int64 overflow")
 63	ErrNegExponent  = errors.New("shuntingyard: negative exponent")
 64	ErrTooLong      = errors.New("shuntingyard: too many tokens")
 65	ErrMalformedRPN = errors.New("shuntingyard: malformed RPN")
 66)
 67
 68// SyntaxError is returned by [Tokenize] and [ToRPN]. Pos is the byte offset in
 69// the source where the problem was noticed, so a caller can point at it.
 70type SyntaxError struct {
 71	Pos int
 72	Msg string
 73}
 74
 75// Error implements the error interface.
 76func (e *SyntaxError) Error() string {
 77	return "shuntingyard: " + e.Msg + " at offset " + strconv.Itoa(e.Pos)
 78}
 79
 80// Kind tells a [Token] apart.
 81type Kind uint8
 82
 83// The four token kinds.
 84const (
 85	KindNumber Kind = iota
 86	KindOperator
 87	KindLParen
 88	KindRParen
 89)
 90
 91// Token is one lexical unit of an expression. Num is meaningful only for
 92// [KindNumber] and Op only for [KindOperator]. Pos is the byte offset the
 93// token started at, kept so an error can name a place rather than a symbol.
 94type Token struct {
 95	Kind Kind
 96	Num  int64
 97	Op   string
 98	Pos  int
 99}
100
101// String renders a token the way it is written in an expression, which is also
102// how [FormatRPN] prints it.
103func (t Token) String() string {
104	switch t.Kind {
105	case KindNumber:
106		return strconv.FormatInt(t.Num, 10)
107	case KindOperator:
108		return t.Op
109	case KindLParen:
110		return "("
111	case KindRParen:
112		return ")"
113	}
114	return "?"
115}
116
117// OpInfo describes one operator: its precedence, whether it associates to the
118// right, and whether it is the prefix unary form.
119type OpInfo struct {
120	Op    string
121	Prec  int
122	Right bool
123	Unary bool
124}
125
126// operators is the whole precedence table. Everything about how an expression
127// groups is in these eight rows, which is the point of the algorithm: the
128// grouping is data, not control flow, so adding an operator is a row.
129//
130// The unary forms sit between the multiplicative operators and the power on
131// purpose. Below the power is what makes -3^2 read as -(3^2), which is how
132// every language that has both spells it; above multiplication is what makes
133// -2*3 read as (-2)*3 rather than -(2*3). The two happen to agree here, but
134// they would not for an operator that is not distributive.
135var operators = []OpInfo{
136	{Op: OpAdd, Prec: 1},
137	{Op: OpSub, Prec: 1},
138	{Op: OpMul, Prec: 2},
139	{Op: OpDiv, Prec: 2},
140	{Op: OpMod, Prec: 2},
141	{Op: OpNeg, Prec: 3, Right: true, Unary: true},
142	{Op: OpPos, Prec: 3, Right: true, Unary: true},
143	{Op: OpPow, Prec: 4, Right: true},
144}
145
146// Operators returns the precedence table, lowest precedence first. The slice
147// is a copy: a caller rendering it must not be able to reorder the parser.
148func Operators() []OpInfo {
149	out := make([]OpInfo, len(operators))
150	copy(out, operators)
151	return out
152}
153
154// lookup returns the table row for an operator spelling.
155func lookup(op string) (OpInfo, bool) {
156	for _, info := range operators {
157		if info.Op == op {
158			return info, true
159		}
160	}
161	return OpInfo{}, false
162}
163
164// Tokenize splits an infix expression into tokens, resolving a leading '+' or
165// '-' to its unary form ([OpPos], [OpNeg]) from what came before it.
166//
167// It validates characters and literals only. Whether the tokens form an
168// expression is [ToRPN]'s question, because that is the pass that knows what
169// may follow what.
170func Tokenize(src string) ([]Token, error) {
171	var out []Token
172	for i := 0; i < len(src); {
173		c := src[i]
174
175		if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
176			i++
177			continue
178		}
179
180		switch {
181		case c >= '0' && c <= '9':
182			j := i
183			for j < len(src) && src[j] >= '0' && src[j] <= '9' {
184				j++
185			}
186			n, err := strconv.ParseInt(src[i:j], 10, 64)
187			if err != nil {
188				return nil, &SyntaxError{Pos: i, Msg: "integer literal out of range"}
189			}
190			out = append(out, Token{Kind: KindNumber, Num: n, Pos: i})
191			i = j
192		case c == '(':
193			out = append(out, Token{Kind: KindLParen, Pos: i})
194			i++
195		case c == ')':
196			out = append(out, Token{Kind: KindRParen, Pos: i})
197			i++
198		default:
199			op := binaryOp(c)
200			if op == "" {
201				return nil, &SyntaxError{Pos: i, Msg: "unexpected character " + quote(src[i:i+1])}
202			}
203			if prefixPosition(out) {
204				switch op {
205				case OpAdd:
206					op = OpPos
207				case OpSub:
208					op = OpNeg
209				}
210			}
211			out = append(out, Token{Kind: KindOperator, Op: op, Pos: i})
212			i++
213		}
214
215		if len(out) > MaxTokens {
216			return nil, ErrTooLong
217		}
218	}
219	return out, nil
220}
221
222// binaryOp maps a byte to the operator it spells, or "" if it spells none.
223func binaryOp(c byte) string {
224	switch c {
225	case '+':
226		return OpAdd
227	case '-':
228		return OpSub
229	case '*':
230		return OpMul
231	case '/':
232		return OpDiv
233	case '%':
234		return OpMod
235	case '^':
236		return OpPow
237	}
238	return ""
239}
240
241// prefixPosition reports whether the next operator would be a prefix one:
242// nothing before it, or an operator or an opening parenthesis before it. A
243// closing parenthesis and a number both end an operand, so what follows them
244// is binary.
245func prefixPosition(out []Token) bool {
246	if len(out) == 0 {
247		return true
248	}
249	prev := out[len(out)-1]
250	return prev.Kind == KindOperator || prev.Kind == KindLParen
251}
252
253// ToRPN converts an infix token stream to reverse Polish notation, rejecting
254// anything that is not a well-formed expression.
255//
256// The validity check is one bit of state: whether an operand or an operator is
257// expected next. That single bit is what rejects "1 2", "1+" and "()", and it
258// costs nothing on top of the conversion the function is doing anyway.
259func ToRPN(in []Token) ([]Token, error) {
260	if len(in) > MaxTokens {
261		return nil, ErrTooLong
262	}
263
264	var out, stack []Token
265	expectOperand := true
266	end := 0
267
268	for _, tok := range in {
269		end = tok.Pos + 1
270
271		switch tok.Kind {
272		case KindNumber:
273			if !expectOperand {
274				return nil, &SyntaxError{Pos: tok.Pos, Msg: "unexpected number"}
275			}
276			out = append(out, tok)
277			expectOperand = false
278
279		case KindLParen:
280			if !expectOperand {
281				return nil, &SyntaxError{Pos: tok.Pos, Msg: "unexpected " + quote("(")}
282			}
283			stack = append(stack, tok)
284
285		case KindRParen:
286			if expectOperand {
287				return nil, &SyntaxError{Pos: tok.Pos, Msg: "unexpected " + quote(")")}
288			}
289			for len(stack) > 0 && stack[len(stack)-1].Kind != KindLParen {
290				out = append(out, stack[len(stack)-1])
291				stack = stack[:len(stack)-1]
292			}
293			if len(stack) == 0 {
294				return nil, &SyntaxError{Pos: tok.Pos, Msg: "unbalanced " + quote(")")}
295			}
296			stack = stack[:len(stack)-1]
297			expectOperand = false
298
299		case KindOperator:
300			info, ok := lookup(tok.Op)
301			if !ok {
302				return nil, &SyntaxError{Pos: tok.Pos, Msg: "unknown operator " + quote(tok.Op)}
303			}
304			// A unary operator is only ever an operand's prefix, a binary one
305			// only ever an operand's successor. One comparison covers both.
306			if info.Unary != expectOperand {
307				return nil, &SyntaxError{Pos: tok.Pos, Msg: "unexpected operator " + quote(tok.Op)}
308			}
309			// A prefix operator pops nothing: whatever is on the stack is
310			// waiting for a right operand that this operator is about to
311			// build, so popping it here would apply it to nothing.
312			if !info.Unary {
313				for len(stack) > 0 {
314					top := stack[len(stack)-1]
315					if top.Kind != KindOperator {
316						break
317					}
318					ti, _ := lookup(top.Op)
319					if ti.Prec > info.Prec || (ti.Prec == info.Prec && !info.Right) {
320						out = append(out, top)
321						stack = stack[:len(stack)-1]
322						continue
323					}
324					break
325				}
326			}
327			stack = append(stack, tok)
328			expectOperand = true
329
330		default:
331			return nil, &SyntaxError{Pos: tok.Pos, Msg: "unknown token"}
332		}
333	}
334
335	if expectOperand {
336		return nil, &SyntaxError{Pos: end, Msg: "unexpected end of expression"}
337	}
338
339	for len(stack) > 0 {
340		top := stack[len(stack)-1]
341		stack = stack[:len(stack)-1]
342		if top.Kind == KindLParen {
343			return nil, &SyntaxError{Pos: top.Pos, Msg: "unbalanced " + quote("(")}
344		}
345		out = append(out, top)
346	}
347	return out, nil
348}
349
350// EvalRPN evaluates a reverse Polish token stream over int64.
351//
352// It re-checks everything [ToRPN] guarantees, because the RPN is the part a
353// realm would store and re-run, and a stored program is an input like any
354// other by the time it is read back.
355func EvalRPN(rpn []Token) (int64, error) {
356	if len(rpn) > MaxTokens {
357		return 0, ErrTooLong
358	}
359
360	var stack []int64
361	for _, tok := range rpn {
362		if tok.Kind == KindNumber {
363			stack = append(stack, tok.Num)
364			continue
365		}
366		if tok.Kind != KindOperator {
367			return 0, ErrMalformedRPN
368		}
369
370		info, ok := lookup(tok.Op)
371		if !ok {
372			return 0, ErrMalformedRPN
373		}
374
375		if info.Unary {
376			if len(stack) < 1 {
377				return 0, ErrMalformedRPN
378			}
379			v, err := applyUnary(tok.Op, stack[len(stack)-1])
380			if err != nil {
381				return 0, err
382			}
383			stack[len(stack)-1] = v
384			continue
385		}
386
387		if len(stack) < 2 {
388			return 0, ErrMalformedRPN
389		}
390		a, b := stack[len(stack)-2], stack[len(stack)-1]
391		stack = stack[:len(stack)-2]
392		v, err := applyBinary(tok.Op, a, b)
393		if err != nil {
394			return 0, err
395		}
396		stack = append(stack, v)
397	}
398
399	if len(stack) != 1 {
400		return 0, ErrMalformedRPN
401	}
402	return stack[0], nil
403}
404
405// Eval tokenizes, converts and evaluates an infix expression in one call.
406func Eval(src string) (int64, error) {
407	toks, err := Tokenize(src)
408	if err != nil {
409		return 0, err
410	}
411	rpn, err := ToRPN(toks)
412	if err != nil {
413		return 0, err
414	}
415	return EvalRPN(rpn)
416}
417
418// FormatRPN renders a token stream space-separated, the usual way RPN is
419// written down.
420func FormatRPN(toks []Token) string {
421	var b strings.Builder
422	for i, t := range toks {
423		if i > 0 {
424			b.WriteString(" ")
425		}
426		b.WriteString(t.String())
427	}
428	return b.String()
429}
430
431// applyUnary applies a prefix operator.
432func applyUnary(op string, a int64) (int64, error) {
433	switch op {
434	case OpNeg:
435		// The one asymmetry of two's complement: there is no positive
436		// counterpart to the smallest int64, so negating it wraps to itself.
437		if a == math.MinInt64 {
438			return 0, ErrOverflow
439		}
440		return -a, nil
441	case OpPos:
442		return a, nil
443	}
444	return 0, ErrMalformedRPN
445}
446
447// applyBinary applies an infix operator, refusing anything it cannot compute
448// exactly.
449func applyBinary(op string, a, b int64) (int64, error) {
450	switch op {
451	case OpAdd:
452		if (b > 0 && a > math.MaxInt64-b) || (b < 0 && a < math.MinInt64-b) {
453			return 0, ErrOverflow
454		}
455		return a + b, nil
456	case OpSub:
457		if (b < 0 && a > math.MaxInt64+b) || (b > 0 && a < math.MinInt64+b) {
458			return 0, ErrOverflow
459		}
460		return a - b, nil
461	case OpMul:
462		return mulChecked(a, b)
463	case OpDiv:
464		if b == 0 {
465			return 0, ErrDivByZero
466		}
467		if a == math.MinInt64 && b == -1 {
468			return 0, ErrOverflow
469		}
470		return a / b, nil
471	case OpMod:
472		if b == 0 {
473			return 0, ErrDivByZero
474		}
475		// MinInt64 % -1 is 0 mathematically, but the hardware instruction
476		// behind it is the same one that overflows on the division.
477		if a == math.MinInt64 && b == -1 {
478			return 0, nil
479		}
480		return a % b, nil
481	case OpPow:
482		return powChecked(a, b)
483	}
484	return 0, ErrMalformedRPN
485}
486
487// mulChecked multiplies, reporting [ErrOverflow] rather than wrapping.
488func mulChecked(a, b int64) (int64, error) {
489	if a == 0 || b == 0 {
490		return 0, nil
491	}
492	if (a == -1 && b == math.MinInt64) || (b == -1 && a == math.MinInt64) {
493		return 0, ErrOverflow
494	}
495	p := a * b
496	// The division is the check: it recovers a exactly unless the product
497	// wrapped. Comparing against a precomputed limit instead needs a different
498	// limit per sign combination, and that is where these get written wrong.
499	if p/b != a {
500		return 0, ErrOverflow
501	}
502	return p, nil
503}
504
505// powChecked raises base to exp by squaring, in O(log exp) checked
506// multiplications.
507func powChecked(base, exp int64) (int64, error) {
508	if exp < 0 {
509		return 0, ErrNegExponent
510	}
511
512	result := int64(1)
513	b, e := base, exp
514	for e > 0 {
515		if e&1 == 1 {
516			v, err := mulChecked(result, b)
517			if err != nil {
518				return 0, err
519			}
520			result = v
521		}
522		e >>= 1
523		// Squaring the base one last time after the final bit is consumed
524		// would overflow on exponents that are perfectly representable, so
525		// the loop stops before the squaring it does not need.
526		if e == 0 {
527			break
528		}
529		v, err := mulChecked(b, b)
530		if err != nil {
531			return 0, err
532		}
533		b = v
534	}
535	return result, nil
536}
537
538// quote wraps a spelling in double quotes for an error message.
539func quote(s string) string {
540	return "\"" + s + "\""
541}