<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sam Vervaeck</title>
    <description>The latest articles on DEV Community by Sam Vervaeck (@samvv).</description>
    <link>https://dev.to/samvv</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1033794%2F708d2df9-ce9b-4fef-9e6a-7434d8547beb.jpeg</url>
      <title>DEV Community: Sam Vervaeck</title>
      <link>https://dev.to/samvv</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/samvv"/>
    <language>en</language>
    <item>
      <title>Generalizing Pratt Parsers, Part 1</title>
      <dc:creator>Sam Vervaeck</dc:creator>
      <pubDate>Sat, 11 Jul 2026 18:08:38 +0000</pubDate>
      <link>https://dev.to/samvv/generalizing-pratt-parsers-part-1-2cmd</link>
      <guid>https://dev.to/samvv/generalizing-pratt-parsers-part-1-2cmd</guid>
      <description>&lt;p&gt;During the design of &lt;a href="https://github.com/samvv/mage" rel="noopener noreferrer"&gt;my parser generator Mage&lt;/a&gt;, I encountered a problem with correctly generating parsers for expressions.&lt;/p&gt;

&lt;p&gt;Let's say we have this definition for an expression in BNF:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;expr ::= int_expr
       | ref_expr
       | add_expr
       | sub_expr
       | mul_expr
       | div_expr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It should parse things like &lt;code&gt;1 + 2&lt;/code&gt;, &lt;code&gt;1 + 2 * 3&lt;/code&gt; and &lt;code&gt;1 / 2 * 3 - 4 + 5&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The closest equivalent grammar in Mage would be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;expr&lt;/span&gt;
  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;add_expr&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;sub_expr&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;mul_expr&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;div_expr&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;int_expr&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;ref_expr&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that the &lt;code&gt;try&lt;/code&gt; keyword in this example attempts to parse whatever comes next to it, and cleanly resets the stream to right before the try-expression was executed. Without it, &lt;code&gt;add_expr&lt;/code&gt; might parse a single &lt;code&gt;int_expr&lt;/code&gt; and advance the stream, but when it fails to parse the &lt;code&gt;+&lt;/code&gt; the stream may remain after the &lt;code&gt;int_expr&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Going further, here are some sample implementations of &lt;code&gt;int_expr&lt;/code&gt; and &lt;code&gt;ref_expr&lt;/code&gt;. We don't need them to be complete; we want them to be just useful enough to experiment with them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;int_expr&lt;/span&gt;
  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;ref_expr&lt;/span&gt;
  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finishing off our tiny grammar, we define four binary expressions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;add_expr&lt;/span&gt;
  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;expr&lt;/span&gt; &lt;span class="sc"&gt;'+'&lt;/span&gt; &lt;span class="n"&gt;expr&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;sub_expr&lt;/span&gt;
  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;expr&lt;/span&gt; &lt;span class="sc"&gt;'-'&lt;/span&gt; &lt;span class="n"&gt;expr&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;mul_expr&lt;/span&gt;
  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;expr&lt;/span&gt; &lt;span class="sc"&gt;'*'&lt;/span&gt; &lt;span class="n"&gt;expr&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;div_expr&lt;/span&gt;
  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;expr&lt;/span&gt; &lt;span class="sc"&gt;'/'&lt;/span&gt; &lt;span class="n"&gt;expr&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Two Problems
&lt;/h2&gt;

&lt;p&gt;We can immediately experience a first problem. This grammar appears to be a valid Mage grammar, but it wil execute forever. Say we give it simply &lt;code&gt;foo&lt;/code&gt; as input. It will attempt to parse &lt;code&gt;expr&lt;/code&gt;, then try &lt;code&gt;add_expr&lt;/code&gt;, followed by &lt;code&gt;expr&lt;/code&gt; and &lt;code&gt;add_expr&lt;/code&gt;, going deeper and deeper and never coming to a halt.&lt;/p&gt;

&lt;p&gt;Moreover, given the input &lt;code&gt;1 + 2 * 3&lt;/code&gt;, this grammar doesn't inform Mage whether we prefer &lt;code&gt;(1 + 2) * 3&lt;/code&gt; or &lt;code&gt;1 + (2 * 3)&lt;/code&gt;. Which one is correct?&lt;/p&gt;

&lt;h2&gt;
  
  
  A Textbook Solution
&lt;/h2&gt;

&lt;p&gt;We always need to parse &lt;em&gt;something&lt;/em&gt;, right? Even if it is a small, tiny little &lt;em&gt;atom&lt;/em&gt;. Let's therefore first define what can be safely parsed without any ugly infinite loops:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;atom&lt;/span&gt;
  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;int_expr&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;ref_expr&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Parsing &lt;code&gt;atom&lt;/code&gt; should never result in the infinite loop we saw earlier. Indeed, it can correctly parse &lt;code&gt;12345&lt;/code&gt; and &lt;code&gt;foobar&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Next, the textbook approach to avoid this infinite loop is to narrow down the possibilities that can be consumed while it descends into &lt;code&gt;expr&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We define &lt;code&gt;factor&lt;/code&gt; as the thing that is parsed within a multiplication, and &lt;code&gt;term&lt;/code&gt; the thing that is parsed within addition or subtraction. After all choices have been exhausted, we delegate &lt;code&gt;term&lt;/code&gt; to &lt;code&gt;factor&lt;/code&gt;, which in turn delegates to &lt;code&gt;atom&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub term
  | try add_expr
  | try sub_expr
  | factor

pub add_expr
  = factor '+' term

pub sub_expr
  = factor '-' term

pub factor
  = try mul_expr
  | try div_expr
  | atom

pub mul_expr
  = atom '*' factor

pub div_expr
  = atom '/' factor

pub expr
  = term
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that a &lt;code&gt;factor&lt;/code&gt; is closer to the 'ground' than a &lt;code&gt;term&lt;/code&gt;, in the sense that it is closer to atomic formulae. This gives it a higher binding power than &lt;code&gt;term&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;When we e.g. parse &lt;code&gt;1 + 2 * 3&lt;/code&gt;, we first parse &lt;code&gt;add_expr&lt;/code&gt;, which succeeds parsing a &lt;code&gt;1&lt;/code&gt; and a &lt;code&gt;+&lt;/code&gt;. We then continue parsing a term that delegates to &lt;code&gt;factor&lt;/code&gt;, which in turn succeeds parsing &lt;code&gt;mul_expr&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pratt Parsing
&lt;/h2&gt;

&lt;p&gt;We could abstract the previous technique to automatically transform Mage grammars so that they once again become executable, but this transformation is far from trivial. There's also the concern for speed: these &lt;code&gt;try&lt;/code&gt; expressions can become overly expensive.&lt;/p&gt;

&lt;p&gt;The question is: how do we generalize this approach and make it efficient? That's where Pratt parsing comes in.&lt;/p&gt;

&lt;p&gt;The most popular article about Pratt parsing seems to be &lt;a href="https://journal.stuffwithstuff.com/2011/03/19/pratt-parsers-expression-parsing-made-easy/" rel="noopener noreferrer"&gt;the one by Bob Nystrom&lt;/a&gt;, the guy who wrote the famous book &lt;em&gt;Crafting Interpreters&lt;/em&gt;. I personally prefer &lt;a href="https://matklad.github.io/2020/04/13/simple-but-powerful-pratt-parsing.html" rel="noopener noreferrer"&gt;the article by Alex Kladov&lt;/a&gt;, one of the maintainers of rust-analyzer.&lt;/p&gt;

&lt;p&gt;A Pratt parser is more of a technique than an algorithm. The defining ingredient of a Pratt parser is &lt;em&gt;precedence&lt;/em&gt;, or as Kladov calls it, &lt;em&gt;binding power&lt;/em&gt;. Sub-expressions with a higher precedence appear deeper in the AST than sub-expressions with a lower precedence.&lt;/p&gt;

&lt;p&gt;This code comes straight from Kladov's article:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;expr_bp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lexer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;Lexer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_bp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;S&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;lhs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;lexer&lt;/span&gt;&lt;span class="nf"&gt;.next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Atom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;S&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Atom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nn"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Op&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'('&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;lhs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;expr_bp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lexer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lexer&lt;/span&gt;&lt;span class="nf"&gt;.next&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nn"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Op&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;')'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
            &lt;span class="n"&gt;lhs&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nn"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Op&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;((),&lt;/span&gt; &lt;span class="n"&gt;r_bp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;prefix_binding_power&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;rhs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;expr_bp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lexer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r_bp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nn"&gt;S&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Cons&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rhs&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nd"&gt;panic!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"bad token: {:?}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;loop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;lexer&lt;/span&gt;&lt;span class="nf"&gt;.peek&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nn"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Eof&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nn"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Op&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nd"&gt;panic!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"bad token: {:?}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;l_bp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()))&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;postfix_binding_power&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;l_bp&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;min_bp&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;lexer&lt;/span&gt;&lt;span class="nf"&gt;.next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;lhs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sc"&gt;'['&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;rhs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;expr_bp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lexer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lexer&lt;/span&gt;&lt;span class="nf"&gt;.next&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nn"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Op&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;']'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
                &lt;span class="nn"&gt;S&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Cons&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;lhs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rhs&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nn"&gt;S&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Cons&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;lhs&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="p"&gt;};&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;l_bp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r_bp&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;infix_binding_power&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;l_bp&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;min_bp&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;lexer&lt;/span&gt;&lt;span class="nf"&gt;.next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;lhs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sc"&gt;'?'&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;mhs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;expr_bp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lexer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lexer&lt;/span&gt;&lt;span class="nf"&gt;.next&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nn"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Op&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;':'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
                &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;rhs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;expr_bp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lexer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r_bp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nn"&gt;S&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Cons&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;lhs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mhs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rhs&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;rhs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;expr_bp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lexer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r_bp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nn"&gt;S&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Cons&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;lhs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rhs&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="p"&gt;};&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;lhs&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;a href="https://gist.github.com/samvv/bb138eaceb253288638d90195f89e654" rel="noopener noreferrer"&gt;this Gist&lt;/a&gt;, I re-implement the algorithm in Python with a few changes.&lt;/p&gt;

&lt;p&gt;First, my implementation uses &lt;code&gt;.tell()&lt;/code&gt; and &lt;code&gt;.seek()&lt;/code&gt; to save and restore the position of the stream right before an operator is going to be parsed. That way, functions like &lt;code&gt;parse_postfix_operator&lt;/code&gt; can become as complex as needed without being required to 'clean up'.&lt;/p&gt;

&lt;p&gt;Second, the functions &lt;code&gt;parse_prefix_operator&lt;/code&gt;, &lt;code&gt;parse_infix_operator&lt;/code&gt; and &lt;code&gt;parse_postfix_operator&lt;/code&gt; return an anonymous function that builds the actual AST node from information they get from the main loop in &lt;code&gt;parse_expr_bp&lt;/code&gt;. This way they aren't limited in what they can parse. Indeed: the entire ternary operator block, a special case in Kladov's code, becomes just another operator in my code. It just so happens to parse more than only the operator, namely an expression and a colon, as if the &lt;code&gt;?&lt;/code&gt; and &lt;code&gt;:&lt;/code&gt; were two parentheses.&lt;/p&gt;

&lt;p&gt;The above two adjustments allow us to write any expression as an operator, just like our Mage metagrammar allows.&lt;/p&gt;

&lt;p&gt;The next challenge therefore isn't with our operators, but with which expressions are allowed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detecting Pratt Expressions
&lt;/h2&gt;

&lt;p&gt;A point I made in the previous section gives us a powerful hint: anything between 'parentheses' (whatever they may be) can be plainly parsed as a top-level expression. See for yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub index_expr
  = expr '[' expr ']'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The precedence of &lt;code&gt;[&lt;/code&gt; matters, but whatever comes after the &lt;code&gt;[&lt;/code&gt; is just part of the operator and doesn't influence precedence at all.&lt;/p&gt;

&lt;p&gt;This means that the only place where precedence matters is at the beginning and end of an expression of a Mage rule. We get in Mage pseudo-code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub prefix_expr
  = operator expr

pub postfix_expr
  = expr operator

pub infix_expr
  = expr operator expr

pub atom_expr
  = any_non_expr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember, however, that a Mage grammar may contain any valid rule and that the developer might not have declared their Pratt-compatible rules as &lt;code&gt;expr&lt;/code&gt;. Therefore we need to find some algorithm to scan the grammar for these rules. Luckily for us, Mage already has some machinery in place that will help us.&lt;/p&gt;

&lt;p&gt;Mage can produce a call graph of an entire grammar, making it easy to spot those rules that indirectly call themselves. Those rules will form a cycle, and so we are tempted to perform a topological sort on this call graph.&lt;/p&gt;

&lt;p&gt;One component coming out of this topological sort will look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu93i46fj3fxpgob82ue1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu93i46fj3fxpgob82ue1.png" alt="Call graph of expr" width="461" height="262"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As expected from a single component, every rule is reachable by every other rule.&lt;/p&gt;

&lt;p&gt;But hold on, what if we have a grammar like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub one
  = '1' two | 'x'

pub two
  = '2' three | 'x'

pub three
  = '3' four | 'x'

pub four
  = '4' one | 'x'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only component will look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F39a5k386cyjzsf9prqr2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F39a5k386cyjzsf9prqr2.png" alt="Call graph of second example" width="421" height="219"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But this example isn't a Pratt expression! Clearly, a Pratt expression is more than a recursive call. A Pratt expression demands choice. Moreover, as we've seen, prefix and postfix require one recursive call at the respective edges while infix requires exactly two.&lt;/p&gt;

&lt;p&gt;We come to the following definition:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A Pratt expression is an expression where at least one of the edges of the expression recursively call themselves, and during that call there is a possible choice to at least one other Pratt expression.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If we didn't have a choice somewhere, there is no need for a Pratt parser.&lt;/p&gt;

&lt;p&gt;This detection mechanism will be implemented in an upcoming version of Mage. I will write a follow-up on the actual implementation of the parser, so stay tuned! &lt;/p&gt;

</description>
    </item>
    <item>
      <title>Migrating The Bolt Programming Language To Rust</title>
      <dc:creator>Sam Vervaeck</dc:creator>
      <pubDate>Mon, 22 Jun 2026 19:07:58 +0000</pubDate>
      <link>https://dev.to/samvv/migrating-the-bolt-programming-language-to-rust-2dhp</link>
      <guid>https://dev.to/samvv/migrating-the-bolt-programming-language-to-rust-2dhp</guid>
      <description>&lt;p&gt;From today and onward I can quite confidently say that Rust is the final implementation language of the &lt;a href="https://github.com/boltlang/bolt" rel="noopener noreferrer"&gt;Bolt programming language&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;After years of trying out different programming languages (from Haskell and OCaml to TypeScript)  and ideas (do I want an ML-style syntax or C-like?), I am very happy with how these experiments are converging. In this search Rust plays a pivotal role.&lt;/p&gt;

&lt;p&gt;Curious? Here's why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Losless syntax trees using Rowan
&lt;/h2&gt;

&lt;p&gt;Real editors not only type-check your code; they also might analyse and transform whitespace, formatting your code according to a certain set of rules.&lt;/p&gt;

&lt;p&gt;Above that, designing syntax trees in Rust can be quite a pain due to the way borrowing and ownership works. For one, it is very difficult to get a simple and safe pointer to the parent node in Rust.&lt;/p&gt;

&lt;p&gt;As opposed to abstract syntax trees, losless syntax trees keep track of this whitespace for you. The &lt;a href="https://crates.io/crates/rowan" rel="noopener noreferrer"&gt;Rowan&lt;/a&gt; libary provides a means for defining such syntax trees. It takes care of ownership and even thread-safety for you, so you can focus on things like type-checking.&lt;/p&gt;

&lt;p&gt;Rowan is based off the concept of red-green trees, where the green nodes contain the data and the red nodes are simple 'views' on this data. The red nodes are constructed whenever the program needs a node of a certain type, while the green nodes are usually constructed during parsing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Robust query engine with Salsa
&lt;/h2&gt;

&lt;p&gt;Another Rust crate that makes your life as a compiler writer that much easier is &lt;a href="https://crates.io/crates/salsa" rel="noopener noreferrer"&gt;Salsa&lt;/a&gt;. It is difficult to work with at first, but once you get the hang of it, it will speed up the development of your compiler tremendously.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0qkydt6t3ndoxdg2ot2i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0qkydt6t3ndoxdg2ot2i.png" alt="The Salsa logo" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Salsa is a tool to build an incremental query-based compiler. With Salsa, there are a few top-level queries one can make about the inputs (such as some programming code written in your language), which get split up in smaller queries. These queries trigger even more queries, all the way down until an answer is formed. The answer then is cached, so that if only a single bit of your inputs change, not everything has to be re-computed from scratch. &lt;/p&gt;

&lt;p&gt;The amazing thing is that you as a compiler designer need not to take into account the million ways of which an input might be changed. Salsa takes care of it. Brilliant!&lt;/p&gt;

&lt;p&gt;I highly recommend to read &lt;a href="(https://ollef.github.io/blog/posts/query-based-compilers.html)"&gt;this article from Olle Fredriksson&lt;/a&gt; and &lt;a href="https://salsa-rs.netlify.app/" rel="noopener noreferrer"&gt;the official Salsa documentation&lt;/a&gt; to learn more!&lt;/p&gt;

&lt;h2&gt;
  
  
  Beautiful error messages with Ariadne
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://codeberg.org/zesterer/ariadne" rel="noopener noreferrer"&gt;Ariadne&lt;/a&gt; is a Rust crate that does all of the dirty work for you when all you want is print a diagnostic message to the terminal. It creates beautiful and very readable error messages, pointing exactly to the location in your source file where the error occurred.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4hp1suzcj4974ka75c6c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4hp1suzcj4974ka75c6c.png" alt="Preview of Ariadne" width="668" height="232"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Thanks to the useful Rust libraries out there, writing a compiler has never been so easy. I will continue to work on my programming language called &lt;a href="https://github.com/boltlang/bolt" rel="noopener noreferrer"&gt;Bolt&lt;/a&gt; using this blueprint. I hope that you might consider it, too!&lt;/p&gt;

</description>
      <category>pldev</category>
      <category>rust</category>
      <category>opensource</category>
      <category>computerscience</category>
    </item>
  </channel>
</rss>
