Evaluate Arithmetic With Variables and Functions
Problem. A program lets users type formulas such as max(x, 2) * 3 ^ 2 - -y / 4, with the usual precedence, and
evaluates them for many values of x and y.
Parse the formula once into a typed tree, then evaluate the tree as often as you like. An Operators Rule gives the
precedence and associativity in a table; the evaluator is a switch on the node type.
// calc.pego
type Num = node[string]
type Name = node[string]
type Bin = { Left Expr; Op node[string]; Right Expr }
type Neg = { X Expr }
type Call = { Fn Name; Args []Expr }
type Expr ~ Num | Name | Bin | Neg | Call
@start
rule main Expr = e:expr ws $$ => $e
rule expr Expr = #operators {
skip ws
operand num
operand call
operand name
operand "(" e:expr ws ")" => $e
level { infix left "+" / "-" => Bin{Left: $lhs, Op: $op, Right: $rhs} }
level { infix left "*" / "/" / "%" => Bin{Left: $lhs, Op: $op, Right: $rhs} }
level { prefix "-" => Neg{X: $rhs} }
level { infix right "^" => Bin{Left: $lhs, Op: $op, Right: $rhs} }
}
rule call Call = f:name ws "(" ws as:args? ws ")" => Call{Fn: $f, Args: concat($as)}
rule args = first:expr rest:(ws "," ws e:expr)* => concat(list($first), map($rest, (r) => r.$e))
rule num Num = ([0-9]+ ("." [0-9]+)?) #atomic
rule name Name = ([a-z_]+) #atomic
rule ws = [ \t\r\n]*
// main.go
package main
import (
_ "embed"
"fmt"
"log"
"math"
"strconv"
"github.com/ornew/pego"
)
//go:embed calc.pego
var grammar string
// funcs are the functions an expression can call, with their number of arguments.
var funcs = map[string]struct {
arity int
f func(a []float64) float64
}{
"sqrt": {1, func(a []float64) float64 { return math.Sqrt(a[0]) }},
"max": {2, func(a []float64) float64 { return math.Max(a[0], a[1]) }},
"min": {2, func(a []float64) float64 { return math.Min(a[0], a[1]) }},
}
// eval evaluates a tree of the grammar with the given values of the variables.
func eval(n *pego.Node, vars map[string]float64) (float64, error) {
switch n.Type() {
case "Num":
return strconv.ParseFloat(n.Text(), 64)
case "Name":
name := n.Text()
v, ok := vars[name]
if !ok {
return 0, fmt.Errorf("undefined variable %q", name)
}
return v, nil
case "Neg":
x, err := eval(n.Field("X").(*pego.Node), vars)
return -x, err
case "Bin":
l, err := eval(n.Field("Left").(*pego.Node), vars)
if err != nil {
return 0, err
}
r, err := eval(n.Field("Right").(*pego.Node), vars)
if err != nil {
return 0, err
}
switch op := n.Field("Op").(*pego.Node).Text(); op {
case "+":
return l + r, nil
case "-":
return l - r, nil
case "*":
return l * r, nil
case "/":
if r == 0 {
return 0, fmt.Errorf("division by zero")
}
return l / r, nil
case "%":
return math.Mod(l, r), nil
default: // "^"
return math.Pow(l, r), nil
}
case "Call":
name := n.Field("Fn").(*pego.Node).Text()
fn, ok := funcs[name]
if !ok {
return 0, fmt.Errorf("undefined function %q", name)
}
argNodes := n.Field("Args").(*pego.Node)
if argNodes.Len() != fn.arity {
return 0, fmt.Errorf("%s takes %d arguments, got %d", name, fn.arity, argNodes.Len())
}
args := make([]float64, argNodes.Len())
for i, a := range argNodes.Elems() {
v, err := eval(a, vars)
if err != nil {
return 0, err
}
args[i] = v
}
return fn.f(args), nil
}
return 0, fmt.Errorf("unexpected %s node", n.Type())
}
func main() {
p, err := pego.CompileSource(grammar, "main")
if err != nil {
log.Fatal(err)
}
// Parse once, evaluate many times.
tree, err := p.Parse("max(x, 2) * 3 ^ 2 ^ 2 - -y / 4")
if err != nil {
log.Fatal(err)
}
for _, x := range []float64{1, 5, 10} {
v, err := eval(tree, map[string]float64{"x": x, "y": 2})
fmt.Println("x =", x, "->", v, err)
}
for _, src := range []string{"1 +", "sqrt(1, 2)", "z * 2", "1 / (2 - 2)", "2 ^ 3 ^ 2", "(1 + 2) * 3 % 4"} {
tree, err := p.Parse(src)
if err != nil {
fmt.Printf("%-16s syntax error: %v\n", src, err)
continue
}
v, err := eval(tree, nil)
fmt.Printf("%-16s %v %v\n", src, v, err)
}
}
x = 1 -> 162.5 <nil>
x = 5 -> 405.5 <nil>
x = 10 -> 810.5 <nil>
1 + syntax error: 1:4: syntax error: expected "(", "-", [0-9], [a-z_]
sqrt(1, 2) 0 sqrt takes 1 arguments, got 2
z * 2 0 undefined variable "z"
1 / (2 - 2) 0 division by zero
2 ^ 3 ^ 2 512 <nil>
(1 + 2) * 3 % 4 1 <nil>
How It Works
- The table is the precedence.
levelblocks run from the loosest to the tightest, so+ -bind loosest, then* / %, then the prefix minus, then^.infix rightmakes^associative to the right (2 ^ 3 ^ 2is2 ^ 9, 512), andinfix leftthe others ((1 + 2) * 3 % 4is9 % 4).-y / 4is(-y) / 4, because the prefix minus binds tighter than/and looser than^. skip wslets white space appear between the tokens of the expression, so the operators and operands need nowsof their own. A rule called from an operand, such ascall, handles its own white space.- Order of operands.
callcomes beforename: both start with a name, and an Operators Rule tries the operands in order, like a choice. - Typed nodes.
Binhas an operator and twoExprs, and the checker rejects a rule that builds one wrongly, when the grammar compiles.Expris a join, soevalcan switch onn.Type()and reach each field withField. - Parse once. The cost of parsing is paid once for
tree; each evaluation only walks it. If you compile the formula into closures instead, as Compile a filter language into a Go predicate does, each evaluation does not even dispatch on the node type. - Errors. A formula that does not parse is a
*pego.SyntaxErrorwith its position. Errors that depend on the values (undefined variable, division by zero) are found while evaluating; wrong arity and undefined functions could also be found in a pass over the tree before any evaluation.
Variations and Limits
- Integer arithmetic (or
big.Int): change theNumconversion and the operators ineval. - Comparison and logic: add levels, loosest first. The filter language does.
- Assignments and statements: put the Operators Rule inside a rule of a statement grammar; the minilang example of examples/ has expressions in a programming language.
- A grammar without an Operators Rule: a rule per precedence level, or a left-recursive rule per level, gives the same trees; examples/calculator has both.
See Operators Rule for operator tables, associativity and level-restricted calls, and Operators Rule for the rules.