---
title: "Precedence levels are dead; long live precedence sets"
slug: precedence-levels-are-dead-long-live-precedence-sets
url: https://listedarticles.com/articles/precedence-levels-are-dead-long-live-precedence-sets
canonical_url: https://www.corsix.org/content/precedence-levels-are-dead
content_type: blog_post
language: en
published_at: 2026-10-27T00:00:00.000Z
updated_at: 2026-10-28T11:11:27.965Z
authored_by: human
publisher: "corsix.org"
publisher_url: https://www.corsix.org/
topics: ["Programming Languages", "Compilers"]
license: all-rights-reserved
word_count: 691
reading_minutes: 3
citation: "corsix.org. \"Precedence levels are dead; long live precedence sets.\" 27 Oct 2026. https://www.corsix.org/content/precedence-levels-are-dead (all-rights-reserved)"
# The full text follows. The web page shows an extract and sends readers
# to the source above; quote the citation and link the canonical URL.
---

# Precedence levels are dead; long live precedence sets

> Prompted by Zig's new partial-order operator precedence, this post explains how parsers can replace numeric precedence levels with precedence sets, showing that a level-based Pratt-style parser can be converted to a set-based one using a few bitwise tricks such as isolating and clearing the lowest set bit.

*corsix.org · October 27, 2026*

It was recently [pointed out on lobste.rs](https://lobste.rs/s/kiivg2/partial_order_precedence) that Zig has overhauled how its parser does operator precedence. The [related documentation](https://ziglang.org/documentation/master/#Operator-Precedence-Flowchart) contains this cute little flowchart of the new operator precedence rules:

```
                     ┏━━━━━━━━━━━━━━━━━━━┓
                     ┃(expr)             ┃
                     ┃x() x[] x.y x.* x.?┃
                     ┃E!T                ┃
                     ┃x{}                ┃
                     ┃!x -x -%x ~x &x ?x ┃
                     ┗━━━━━━━━━▲━━━━━━━━━┛
          ┌─────────────┬──────┴───────┬───────┬──┬──┬─┐
          │             │              │       │  │  │ │
   ┏━━━━━━┷━━━━━┓╭Non╌Chaining╮ ╭Non╌Chaining╮┏┷┓┏┷┓┏┷┓│
   ┃* *% *| / ||┃╎      %     ╎ ╎ << <<|  >> ╎┃&┃┃^┃┃|┃│
   ┗━━━━━━▲━━━━━┛╰╌╌╌╌╌╌▲╌╌╌╌╌╯ ╰╌╌╌╌╌╌▲╌╌╌╌╌╯┗▲┛┗▲┛┗▲┛│
       ┏━━┷━━━━━━━━━━━━━┷━┓       ┌────┴───────┴──┴──┘ │
       ┃+ - +% -% +| -| ++┃       │                    │
       ┗━━━━━━━━━━━▲━━━━━━┛       │                    │
                  ╭┴╌Non-Chaining╌┴╮                   │
                  ╎== !=  < <= >= >╎                   │
                  ╰╌╌╌▲╌╌╌╌▲╌╌╌╌╌╌╌╯      ┌──────┬─────┘
                    ┏━┷━┓┏━┷━┓        ┏━━━┷━━┓┏━━┷━━┓
                    ┃and┃┃or ┃        ┃orelse┃┃catch┃
                    ┗━▲━┛┗━▲━┛        ┗━━━▲━━┛┗━━▲━━┛
                   ╭╌╌┴╌╌╌╌┴╌╌╌╌╌╌╌╌╌╌╌╌╌╌┴╌╌╌╌╌╌┴╌╮
                   ╎    Any expression location    ╎
                   ╰╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╯
```

If you're not familiar with Zig, some of these operators might be foreign to you. By removing some of the more exotic operators, and doing a few other tidy-ups, we get this flowchart:

```
                       ┏━━━━━━━━━━━━━━━┓
                       ┃(e) ident const┃
                       ┗━━━━━━━▲━━━━━━━┛
                ┏━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━┓
                ┃x(e,…,e) x[e] x.ident x.* x.?┃
                ┗━━━━━━━━━━━━━━▲━━━━━━━━━━━━━━┛
                       ┏━━━━━━━┷━━━━━━━┓
                       ┃!x -x ~x &x ?x ┃
                       ┗━━━━━━━▲━━━━━━━┛
           ┌─────┬─────────────┴──┬────────┬────┬────┬──┐
           │     │                │        │    │    │  │
    ┏━━━━━━┷┓╭Non┴Chaining╮╭Non╌Chaining╮┏━┷━┓┏━┷━┓┏━┷━┓│
    ┃x*y x/y┃╎     x%y    ╎╎ x<<y  x>>y ╎┃x&y┃┃x^y┃┃x|y┃│
    ┗━━━━━━▲┛╰╌╌╌▲╌╌╌╌╌╌╌╌╯╰╌╌╌╌╌╌▲╌╌╌╌╌╯┗━▲━┛┗━▲━┛┗━▲━┛│
          ┏┷━━━━━┷┓              ┌┴────────┴────┴────┘  │
          ┃x+y x-y┃              │                      │
          ┗━━━▲━━━┛              │                      │
       ╭╌╌╌╌╌╌┴╌╌╌Non-Chaining╌╌╌┴╌╮                    │
       ╎x==y x!=y x<y x<=y x>=y x>y╎                    │
       ╰╌╌╌╌╌╌╌╌╌╌╌╌▲╌╌╌╌╌╌╌▲╌╌╌╌╌╌╯       ┌────────────┘
                ┏━━━┷━━━┓┏━━┷━━━┓     ┏━━━━┷━━━━━┓
                ┃x and y┃┃x or y┃     ┃x orelse y┃
                ┗━━━▲━━━┛┗━━▲━━━┛     ┗━━━━▲━━━━━┛
                ╭╌╌╌┴╌╌╌╌╌╌╌┴╌╌╌╌╌╌╌╌╌╌╌╌╌╌┴╌╌╌╌╌╮
                ╎    Any expression location     ╎
                ╰╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╯
```

I vastly prefer this presentation format over a table of operators which lists facts such as "addition has precedence level 5, exclusive-or has precedence level 7", but just in case it isn't clear, the meaning of this flowchart is:

- In general, an expression can consist of anything reachable from the "*Any expression location*" box.
- Within a thick-bordered box, `x` and `y` represent expressions, which can come from the same box or from any box reachable from it (if being pedantic about associativity, say that `x` can come from the same box but `y` cannot).
- Thin-bordered "*Non-Chaining*" boxes are similar, except that neither `x` nor `y` can come from the same box.
- Within any box, `e` represents a general expression (i.e. restart from the "*Any expression location*" box).

If implementing a language, the question arises of how to translate this chart into a parser. There is plently of literature on how to translate precedence *levels* into a parser; see for example [Pratt Parsing Index and Updates](https://www.oilshell.org/blog/2017/03/31.html). If following the concrete exposition in [Crafting Interpreters](https://github.com/munificent/craftinginterpreters/blob/4a840f70f69c6ddd17cfef4f6964f8e1bcd8c3d4/c/compiler.c#L36-L48), we'd give each box in the flowchart a unique small integer, and end up with something like:

```
prec_primary    = 14 # (e) ident const
prec_call       = 13 # x(e,…,e) x[e] x.ident x.* x.?
prec_unary      = 12 # !x -x ~x &x ?x
prec_factor     = 11 # x*y x/y
prec_mod        = 10 # x%y
prec_shift      =  9 # x<<y x>>y
prec_bit_and    =  8 # x&y
prec_bit_xor    =  7 # x^y
prec_bit_or     =  6 # x|y
prec_term       =  5 # x+y x-y
prec_comparison =  4 # x==y x!=y x<y x<=y x>=y x>y
prec_and        =  3 # x and y
prec_or         =  2 # x or y
prec_orelse     =  1 # x orelse y
prec_none       =  0 # Any expression location
```

This approach works great for precedence levels, but the flowchart fundamentally cannot be represented using precedence levels. Instead, we need precedence *sets*:

```
prec_primary    = 0b100000000000000 # (e) ident const
prec_call       = 0b110000000000000 # x(e,…,e) x[e] x.ident x.* x.?
prec_unary      = 0b111000000000000 # !x -x ~x &x ?x
prec_factor     = 0b111100000000000 # x*y x/y
prec_mod        = 0b111010000000000 # x%y
prec_shift      = 0b111001000000000 # x<<y x>>y
prec_bit_and    = 0b111000100000000 # x&y
prec_bit_xor    = 0b111000010000000 # x^y
prec_bit_or     = 0b111000001000000 # x|y
prec_term       = 0b111110000100000 # x+y x-y
prec_comparison = 0b111111111110000 # x==y x!=y x<y x<=y x>=y x>y
prec_and        = 0b111111111111000 # x and y
prec_or         = 0b111111111110100 # x or y
prec_orelse     = 0b111000000000010 # x orelse y
prec_none       = 0b111111111111111 # Any expression location
```

> If this looks to you like the reachability matrix of the flowchart, well done: it is exactly the reachability matrix of the flowchart.

Precedence *levels* can be manipulated in a parser using arithmetic operators: Crafting Interpreters contains [`precedence + 1`](https://github.com/munificent/craftinginterpreters/blob/4a840f70f69c6ddd17cfef4f6964f8e1bcd8c3d4/c/compiler.c#L605) and [`precedence <=`](https://github.com/munificent/craftinginterpreters/blob/4a840f70f69c6ddd17cfef4f6964f8e1bcd8c3d4/c/compiler.c#L1001) in key places. Precedence *sets* instead require bitwise tricks: isolate lowest set bit, clear lowest set bit, and bitwise and. Any level-based parser can be converted to a set-based parser by suitable replacement of arithmetic with these bitwise tricks, and I hope more languages follow Zig's lead here, freeing themselves from the tyranny of precedence levels and instead adopting precedence sets.

---

*Originally published at [corsix.org](https://www.corsix.org/content/precedence-levels-are-dead).*
