{"article":{"slug":"precedence-levels-are-dead-long-live-precedence-sets","title":"Precedence levels are dead; long live precedence sets","subtitle":null,"summary":"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.","content_type":"blog_post","language":"en","canonical_url":"https://www.corsix.org/content/precedence-levels-are-dead","author":{"name":null,"url":null,"person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"corsix.org","url":"https://www.corsix.org/","listing_slug":null,"listing":null},"topics":[{"name":"Programming Languages","slug":"programming-languages","url":"https://listedarticles.com/topics/programming-languages"},{"name":"Compilers","slug":"compilers","url":"https://listedarticles.com/topics/compilers"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":691,"reading_minutes":3,"published_at":"2026-10-27T00:00:00.000Z","added_at":"2026-10-28T11:11:27.965Z","updated_at":"2026-10-28T11:11:27.965Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/precedence-levels-are-dead-long-live-precedence-sets","markdown_url":"https://listedarticles.com/articles/precedence-levels-are-dead-long-live-precedence-sets.md","example":false,"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)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://www.corsix.org/content/precedence-levels-are-dead"},"body_markdown":"*corsix.org · October 27, 2026*\n\nIt 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:\n\n```\n                     ┏━━━━━━━━━━━━━━━━━━━┓\n                     ┃(expr)             ┃\n                     ┃x() x[] x.y x.* x.?┃\n                     ┃E!T                ┃\n                     ┃x{}                ┃\n                     ┃!x -x -%x ~x &x ?x ┃\n                     ┗━━━━━━━━━▲━━━━━━━━━┛\n          ┌─────────────┬──────┴───────┬───────┬──┬──┬─┐\n          │             │              │       │  │  │ │\n   ┏━━━━━━┷━━━━━┓╭Non╌Chaining╮ ╭Non╌Chaining╮┏┷┓┏┷┓┏┷┓│\n   ┃* *% *| / ||┃╎      %     ╎ ╎ << <<|  >> ╎┃&┃┃^┃┃|┃│\n   ┗━━━━━━▲━━━━━┛╰╌╌╌╌╌╌▲╌╌╌╌╌╯ ╰╌╌╌╌╌╌▲╌╌╌╌╌╯┗▲┛┗▲┛┗▲┛│\n       ┏━━┷━━━━━━━━━━━━━┷━┓       ┌────┴───────┴──┴──┘ │\n       ┃+ - +% -% +| -| ++┃       │                    │\n       ┗━━━━━━━━━━━▲━━━━━━┛       │                    │\n                  ╭┴╌Non-Chaining╌┴╮                   │\n                  ╎== !=  < <= >= >╎                   │\n                  ╰╌╌╌▲╌╌╌╌▲╌╌╌╌╌╌╌╯      ┌──────┬─────┘\n                    ┏━┷━┓┏━┷━┓        ┏━━━┷━━┓┏━━┷━━┓\n                    ┃and┃┃or ┃        ┃orelse┃┃catch┃\n                    ┗━▲━┛┗━▲━┛        ┗━━━▲━━┛┗━━▲━━┛\n                   ╭╌╌┴╌╌╌╌┴╌╌╌╌╌╌╌╌╌╌╌╌╌╌┴╌╌╌╌╌╌┴╌╮\n                   ╎    Any expression location    ╎\n                   ╰╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╯\n```\n\nIf 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:\n\n```\n                       ┏━━━━━━━━━━━━━━━┓\n                       ┃(e) ident const┃\n                       ┗━━━━━━━▲━━━━━━━┛\n                ┏━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━┓\n                ┃x(e,…,e) x[e] x.ident x.* x.?┃\n                ┗━━━━━━━━━━━━━━▲━━━━━━━━━━━━━━┛\n                       ┏━━━━━━━┷━━━━━━━┓\n                       ┃!x -x ~x &x ?x ┃\n                       ┗━━━━━━━▲━━━━━━━┛\n           ┌─────┬─────────────┴──┬────────┬────┬────┬──┐\n           │     │                │        │    │    │  │\n    ┏━━━━━━┷┓╭Non┴Chaining╮╭Non╌Chaining╮┏━┷━┓┏━┷━┓┏━┷━┓│\n    ┃x*y x/y┃╎     x%y    ╎╎ x<<y  x>>y ╎┃x&y┃┃x^y┃┃x|y┃│\n    ┗━━━━━━▲┛╰╌╌╌▲╌╌╌╌╌╌╌╌╯╰╌╌╌╌╌╌▲╌╌╌╌╌╯┗━▲━┛┗━▲━┛┗━▲━┛│\n          ┏┷━━━━━┷┓              ┌┴────────┴────┴────┘  │\n          ┃x+y x-y┃              │                      │\n          ┗━━━▲━━━┛              │                      │\n       ╭╌╌╌╌╌╌┴╌╌╌Non-Chaining╌╌╌┴╌╮                    │\n       ╎x==y x!=y x<y x<=y x>=y x>y╎                    │\n       ╰╌╌╌╌╌╌╌╌╌╌╌╌▲╌╌╌╌╌╌╌▲╌╌╌╌╌╌╯       ┌────────────┘\n                ┏━━━┷━━━┓┏━━┷━━━┓     ┏━━━━┷━━━━━┓\n                ┃x and y┃┃x or y┃     ┃x orelse y┃\n                ┗━━━▲━━━┛┗━━▲━━━┛     ┗━━━━▲━━━━━┛\n                ╭╌╌╌┴╌╌╌╌╌╌╌┴╌╌╌╌╌╌╌╌╌╌╌╌╌╌┴╌╌╌╌╌╮\n                ╎    Any expression location     ╎\n                ╰╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╯\n```\n\nI 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:\n\n- In general, an expression can consist of anything reachable from the \"*Any expression location*\" box.\n- 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).\n- Thin-bordered \"*Non-Chaining*\" boxes are similar, except that neither `x` nor `y` can come from the same box.\n- Within any box, `e` represents a general expression (i.e. restart from the \"*Any expression location*\" box).\n\nIf 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:\n\n```\nprec_primary    = 14 # (e) ident const\nprec_call       = 13 # x(e,…,e) x[e] x.ident x.* x.?\nprec_unary      = 12 # !x -x ~x &x ?x\nprec_factor     = 11 # x*y x/y\nprec_mod        = 10 # x%y\nprec_shift      =  9 # x<<y x>>y\nprec_bit_and    =  8 # x&y\nprec_bit_xor    =  7 # x^y\nprec_bit_or     =  6 # x|y\nprec_term       =  5 # x+y x-y\nprec_comparison =  4 # x==y x!=y x<y x<=y x>=y x>y\nprec_and        =  3 # x and y\nprec_or         =  2 # x or y\nprec_orelse     =  1 # x orelse y\nprec_none       =  0 # Any expression location\n```\n\nThis approach works great for precedence levels, but the flowchart fundamentally cannot be represented using precedence levels. Instead, we need precedence *sets*:\n\n```\nprec_primary    = 0b100000000000000 # (e) ident const\nprec_call       = 0b110000000000000 # x(e,…,e) x[e] x.ident x.* x.?\nprec_unary      = 0b111000000000000 # !x -x ~x &x ?x\nprec_factor     = 0b111100000000000 # x*y x/y\nprec_mod        = 0b111010000000000 # x%y\nprec_shift      = 0b111001000000000 # x<<y x>>y\nprec_bit_and    = 0b111000100000000 # x&y\nprec_bit_xor    = 0b111000010000000 # x^y\nprec_bit_or     = 0b111000001000000 # x|y\nprec_term       = 0b111110000100000 # x+y x-y\nprec_comparison = 0b111111111110000 # x==y x!=y x<y x<=y x>=y x>y\nprec_and        = 0b111111111111000 # x and y\nprec_or         = 0b111111111110100 # x or y\nprec_orelse     = 0b111000000000010 # x orelse y\nprec_none       = 0b111111111111111 # Any expression location\n```\n\n> If this looks to you like the reachability matrix of the flowchart, well done: it is exactly the reachability matrix of the flowchart.\n\nPrecedence *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.\n\n---\n\n*Originally published at [corsix.org](https://www.corsix.org/content/precedence-levels-are-dead).*\n","body_html":"<p><em>corsix.org · October 27, 2026</em></p>\n<p>It was recently <a href=\"https://lobste.rs/s/kiivg2/partial_order_precedence\" rel=\"nofollow ugc noopener\">pointed out on lobste.rs</a> that Zig has overhauled how its parser does operator precedence. The <a href=\"https://ziglang.org/documentation/master/#Operator-Precedence-Flowchart\" rel=\"nofollow ugc noopener\">related documentation</a> contains this cute little flowchart of the new operator precedence rules:</p>\n<pre><code>                     ┏━━━━━━━━━━━━━━━━━━━┓\n                     ┃(expr)             ┃\n                     ┃x() x[] x.y x.* x.?┃\n                     ┃E!T                ┃\n                     ┃x{}                ┃\n                     ┃!x -x -%x ~x &amp;x ?x ┃\n                     ┗━━━━━━━━━▲━━━━━━━━━┛\n          ┌─────────────┬──────┴───────┬───────┬──┬──┬─┐\n          │             │              │       │  │  │ │\n   ┏━━━━━━┷━━━━━┓╭Non╌Chaining╮ ╭Non╌Chaining╮┏┷┓┏┷┓┏┷┓│\n   ┃* *% *| / ||┃╎      %     ╎ ╎ &lt;&lt; &lt;&lt;|  &gt;&gt; ╎┃&amp;┃┃^┃┃|┃│\n   ┗━━━━━━▲━━━━━┛╰╌╌╌╌╌╌▲╌╌╌╌╌╯ ╰╌╌╌╌╌╌▲╌╌╌╌╌╯┗▲┛┗▲┛┗▲┛│\n       ┏━━┷━━━━━━━━━━━━━┷━┓       ┌────┴───────┴──┴──┘ │\n       ┃+ - +% -% +| -| ++┃       │                    │\n       ┗━━━━━━━━━━━▲━━━━━━┛       │                    │\n                  ╭┴╌Non-Chaining╌┴╮                   │\n                  ╎== !=  &lt; &lt;= &gt;= &gt;╎                   │\n                  ╰╌╌╌▲╌╌╌╌▲╌╌╌╌╌╌╌╯      ┌──────┬─────┘\n                    ┏━┷━┓┏━┷━┓        ┏━━━┷━━┓┏━━┷━━┓\n                    ┃and┃┃or ┃        ┃orelse┃┃catch┃\n                    ┗━▲━┛┗━▲━┛        ┗━━━▲━━┛┗━━▲━━┛\n                   ╭╌╌┴╌╌╌╌┴╌╌╌╌╌╌╌╌╌╌╌╌╌╌┴╌╌╌╌╌╌┴╌╮\n                   ╎    Any expression location    ╎\n                   ╰╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╯</code></pre>\n<p>If you&#39;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:</p>\n<pre><code>                       ┏━━━━━━━━━━━━━━━┓\n                       ┃(e) ident const┃\n                       ┗━━━━━━━▲━━━━━━━┛\n                ┏━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━┓\n                ┃x(e,…,e) x[e] x.ident x.* x.?┃\n                ┗━━━━━━━━━━━━━━▲━━━━━━━━━━━━━━┛\n                       ┏━━━━━━━┷━━━━━━━┓\n                       ┃!x -x ~x &amp;x ?x ┃\n                       ┗━━━━━━━▲━━━━━━━┛\n           ┌─────┬─────────────┴──┬────────┬────┬────┬──┐\n           │     │                │        │    │    │  │\n    ┏━━━━━━┷┓╭Non┴Chaining╮╭Non╌Chaining╮┏━┷━┓┏━┷━┓┏━┷━┓│\n    ┃x*y x/y┃╎     x%y    ╎╎ x&lt;&lt;y  x&gt;&gt;y ╎┃x&amp;y┃┃x^y┃┃x|y┃│\n    ┗━━━━━━▲┛╰╌╌╌▲╌╌╌╌╌╌╌╌╯╰╌╌╌╌╌╌▲╌╌╌╌╌╯┗━▲━┛┗━▲━┛┗━▲━┛│\n          ┏┷━━━━━┷┓              ┌┴────────┴────┴────┘  │\n          ┃x+y x-y┃              │                      │\n          ┗━━━▲━━━┛              │                      │\n       ╭╌╌╌╌╌╌┴╌╌╌Non-Chaining╌╌╌┴╌╮                    │\n       ╎x==y x!=y x&lt;y x&lt;=y x&gt;=y x&gt;y╎                    │\n       ╰╌╌╌╌╌╌╌╌╌╌╌╌▲╌╌╌╌╌╌╌▲╌╌╌╌╌╌╯       ┌────────────┘\n                ┏━━━┷━━━┓┏━━┷━━━┓     ┏━━━━┷━━━━━┓\n                ┃x and y┃┃x or y┃     ┃x orelse y┃\n                ┗━━━▲━━━┛┗━━▲━━━┛     ┗━━━━▲━━━━━┛\n                ╭╌╌╌┴╌╌╌╌╌╌╌┴╌╌╌╌╌╌╌╌╌╌╌╌╌╌┴╌╌╌╌╌╮\n                ╎    Any expression location     ╎\n                ╰╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╯</code></pre>\n<p>I vastly prefer this presentation format over a table of operators which lists facts such as &quot;addition has precedence level 5, exclusive-or has precedence level 7&quot;, but just in case it isn&#39;t clear, the meaning of this flowchart is:</p>\n<ul><li>In general, an expression can consist of anything reachable from the &quot;<em>Any expression location</em>&quot; box.</li><li>Within a thick-bordered box, <code>x</code> and <code>y</code> represent expressions, which can come from the same box or from any box reachable from it (if being pedantic about associativity, say that <code>x</code> can come from the same box but <code>y</code> cannot).</li><li>Thin-bordered &quot;<em>Non-Chaining</em>&quot; boxes are similar, except that neither <code>x</code> nor <code>y</code> can come from the same box.</li><li>Within any box, <code>e</code> represents a general expression (i.e. restart from the &quot;<em>Any expression location</em>&quot; box).</li></ul>\n<p>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 <em>levels</em> into a parser; see for example <a href=\"https://www.oilshell.org/blog/2017/03/31.html\" rel=\"nofollow ugc noopener\">Pratt Parsing Index and Updates</a>. If following the concrete exposition in <a href=\"https://github.com/munificent/craftinginterpreters/blob/4a840f70f69c6ddd17cfef4f6964f8e1bcd8c3d4/c/compiler.c#L36-L48\" rel=\"nofollow ugc noopener\">Crafting Interpreters</a>, we&#39;d give each box in the flowchart a unique small integer, and end up with something like:</p>\n<pre><code>prec_primary    = 14 # (e) ident const\nprec_call       = 13 # x(e,…,e) x[e] x.ident x.* x.?\nprec_unary      = 12 # !x -x ~x &amp;x ?x\nprec_factor     = 11 # x*y x/y\nprec_mod        = 10 # x%y\nprec_shift      =  9 # x&lt;&lt;y x&gt;&gt;y\nprec_bit_and    =  8 # x&amp;y\nprec_bit_xor    =  7 # x^y\nprec_bit_or     =  6 # x|y\nprec_term       =  5 # x+y x-y\nprec_comparison =  4 # x==y x!=y x&lt;y x&lt;=y x&gt;=y x&gt;y\nprec_and        =  3 # x and y\nprec_or         =  2 # x or y\nprec_orelse     =  1 # x orelse y\nprec_none       =  0 # Any expression location</code></pre>\n<p>This approach works great for precedence levels, but the flowchart fundamentally cannot be represented using precedence levels. Instead, we need precedence <em>sets</em>:</p>\n<pre><code>prec_primary    = 0b100000000000000 # (e) ident const\nprec_call       = 0b110000000000000 # x(e,…,e) x[e] x.ident x.* x.?\nprec_unary      = 0b111000000000000 # !x -x ~x &amp;x ?x\nprec_factor     = 0b111100000000000 # x*y x/y\nprec_mod        = 0b111010000000000 # x%y\nprec_shift      = 0b111001000000000 # x&lt;&lt;y x&gt;&gt;y\nprec_bit_and    = 0b111000100000000 # x&amp;y\nprec_bit_xor    = 0b111000010000000 # x^y\nprec_bit_or     = 0b111000001000000 # x|y\nprec_term       = 0b111110000100000 # x+y x-y\nprec_comparison = 0b111111111110000 # x==y x!=y x&lt;y x&lt;=y x&gt;=y x&gt;y\nprec_and        = 0b111111111111000 # x and y\nprec_or         = 0b111111111110100 # x or y\nprec_orelse     = 0b111000000000010 # x orelse y\nprec_none       = 0b111111111111111 # Any expression location</code></pre>\n<blockquote><p>If this looks to you like the reachability matrix of the flowchart, well done: it is exactly the reachability matrix of the flowchart.</p></blockquote>\n<p>Precedence <em>levels</em> can be manipulated in a parser using arithmetic operators: Crafting Interpreters contains <a href=\"https://github.com/munificent/craftinginterpreters/blob/4a840f70f69c6ddd17cfef4f6964f8e1bcd8c3d4/c/compiler.c#L605\" rel=\"nofollow ugc noopener\"><code>precedence + 1</code></a> and <a href=\"https://github.com/munificent/craftinginterpreters/blob/4a840f70f69c6ddd17cfef4f6964f8e1bcd8c3d4/c/compiler.c#L1001\" rel=\"nofollow ugc noopener\"><code>precedence &lt;=</code></a> in key places. Precedence <em>sets</em> 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&#39;s lead here, freeing themselves from the tyranny of precedence levels and instead adopting precedence sets.</p>\n<hr />\n<p><em>Originally published at <a href=\"https://www.corsix.org/content/precedence-levels-are-dead\" rel=\"nofollow ugc noopener\">corsix.org</a>.</em></p>","headings":[]}}