Lesson 2.2

Operator Precedence

Why 1+2*3 is 7, not 9. How the parser encodes math rules into tree shape.

concepts 15 min read

The flat token problem.

The lexer gives us tokens in order. But order alone doesn't tell us grouping. Consider:

a + b * c - d

Four tokens, three operators. Which operations happen first? In math class, you learned PEMDAS — multiplication before addition. But the parser doesn't know PEMDAS. It needs a systematic way to encode these rules into the tree it builds.

The answer: precedence levels. Each operator gets a number. Higher number = binds tighter = goes deeper in the tree. The parser uses these numbers to decide which operators "grab" their operands first.

Monk's 13 precedence levels.

From loosest (evaluated last) to tightest (evaluated first):

Level Name Operators Assoc.
1 Assignment = += -= *= /= %= Right
2 Logical OR || or Left
3 Logical AND && and Left
4 Bitwise OR | Left
5 Bitwise XOR ^ Left
6 Bitwise AND & Left
7 Equality == != is Left
8 Comparison < > <= >= Left
9 Shift << >> Left
10 Addition + - Left
11 Multiplication * / % Left
12 Unary - ! ~ not Right
13 Call / Index () [] . Left

Level 1 is the loosest — assignment happens after everything else on the right side is computed. Level 13 is the tightest — function calls and property access bind before any arithmetic.

The tree shape IS the precedence. A level-11 operator (multiplication) ends up deeper in the tree than a level-10 operator (addition). Deeper means evaluated first. The precedence table is literally encoded as tree depth.

Left vs right associativity.

Precedence tells you which operator goes first when they're different. Associativity tells you which goes first when they're the same.

Left-associative (most operators): a - b - c means (a - b) - c. The leftmost operation happens first.

Right-associative (assignment, unary): a = b = c would mean a = (b = c). The rightmost operation happens first. But in Monk, assignment is a statement — you can't chain assignments like this. Unary operators are naturally right-associative: --x means -(-(x)).

Left-associative: 8 - 3 - 2 = (8 - 3) - 2 = 3

BinaryExpr (-)
BinaryExpr (-)
NumberExpr (8)
NumberExpr (3)
NumberExpr (2)

The left subtree is deeper. The left subtraction evaluates first: 8 - 3 = 5, then 5 - 2 = 3. If it were right-associative, you'd get 8 - (3 - 2) = 7 — wrong answer.

How recursive descent encodes precedence.

The elegant trick: write one function per precedence level. Each function handles operators at its level, and calls the next-higher-level function for its operands. This naturally creates the right tree shape.

// Level 10: addition and subtraction
func parseAddition() Node {
    left := parseMultiplication()  // call the next level up
    while currentToken is PLUS or MINUS {
        op := consume()
        right := parseMultiplication()
        left = BinaryExpr(left, op, right)
    }
    return left
}

// Level 11: multiplication and division
func parseMultiplication() Node {
    left := parseUnary()  // call the next level up
    while currentToken is STAR or SLASH or PERCENT {
        op := consume()
        right := parseUnary()
        left = BinaryExpr(left, op, right)
    }
    return left
}

When parseAddition needs an operand, it calls parseMultiplication. That function will consume any * or / operators before returning. So by the time parseAddition builds its node, all the multiplications are already buried deeper in the tree.

The call stack IS the precedence. Each recursive call goes one level deeper. Multiplication's function is called by addition's function, so multiplication nodes end up as children (deeper) in the tree. The programming language's call stack mirrors the AST's nesting.

Tracing through a + b * c - d.

Let's trace the recursive calls step by step:

parseAddition() Calls parseMultiplication() for left operand
parseMultiplication() Calls parseUnary(), gets IdentExpr(a). No * or / next, returns a
parseAddition() Sees PLUS. Consumes it. Calls parseMultiplication() for right operand
parseMultiplication() Calls parseUnary(), gets IdentExpr(b). Sees STAR next!
parseMultiplication() Consumes STAR. Calls parseUnary(), gets IdentExpr(c). Builds BinaryExpr(b * c)
parseMultiplication() No more * or /. Returns BinaryExpr(b * c)
parseAddition() Builds BinaryExpr(a + (b*c)). Sees MINUS. Consumes it.
parseMultiplication() Calls parseUnary(), gets IdentExpr(d). Returns d
parseAddition() Builds BinaryExpr((a + b*c) - d). Done.

The key moment: when parseAddition asks for the right operand of +, it calls parseMultiplication, which gobbles up the b * c into a single node. By the time control returns to parseAddition, the multiplication is already done and wrapped in a subtree.

Precedence climbing vs Pratt parsing.

What we described above — one function per level, each calling the next — is called precedence climbing. It's the most natural approach in recursive descent parsers.

An alternative is Pratt parsing (also called "top-down operator precedence"). Instead of one function per level, you have a single function with a precedence parameter. Operators register their binding power in a table, and the parser uses the table to decide when to stop.

Precedence climbing 13 functions, each trivially simple. Easy to read, easy to debug. Monk uses this.
Pratt parsing 1 function, driven by a table. More compact. Harder to step through in a debugger.

Both produce identical trees. The difference is code organization, not output. Monk chose precedence climbing because each level is a simple function you can set a breakpoint in. When something parses wrong, you know exactly which level to look at.

Key takeaways

1

Precedence is encoded as tree depth. Higher-precedence operators end up deeper and evaluate first.

2

Monk has 13 precedence levels, from assignment (loosest) to call/index/property access (tightest).

3

Left-associativity means a - b - c = (a - b) - c. Implemented with a while loop that builds left-to-right.

4

Recursive descent with precedence climbing: one function per level, each calling the next. The call stack mirrors the tree depth.