<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Programming on Kyle W. Baldwin</title><link>https://kylebaldw.in/categories/programming/</link><description>Recent content in Programming on Kyle W. Baldwin</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Mon, 31 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://kylebaldw.in/categories/programming/index.xml" rel="self" type="application/rss+xml"/><item><title>Building a LilyPond Parser - Part 1</title><link>https://kylebaldw.in/blog/building-a-lilypond-parser-part-1/</link><pubDate>Mon, 31 Aug 2026 00:00:00 +0000</pubDate><guid>https://kylebaldw.in/blog/building-a-lilypond-parser-part-1/</guid><description>&lt;p&gt;After diving into compiler and parser creation with&#10;&lt;a class="link" href="https://craftinginterpreters.com/" target="_blank" rel="noopener"&#10; &gt;Crafting Interpreters&lt;/a&gt;, I wanted to apply it&#10;to something that would be helpful to me. As I do a lot of working with musical&#10;scores in &lt;a class="link" href="https://lilypond.org/" target="_blank" rel="noopener"&#10; &gt;LilyPond&lt;/a&gt;, I thought how great it would be to&#10;have an LSP (Language Server Protocol) server to help detect errors before I hit&#10;&amp;ldquo;compile.&amp;rdquo;&lt;/p&gt;&#10;&lt;h2 id="the-state-is-the-problem"&gt;The state is the problem&#10;&lt;/h2&gt;&lt;p&gt;Looking at the built-in lexer and parser, there are some significant challenges.&#10;The first one, and the largest one I anticipate dealing with, is that the Flex&#10;lexer and Bison parser are constantly communicating with each other about the&#10;state of the lexing. The lexer defines thirteen definitive states, however some&#10;of them are not interacting with the lexical states and rather just telling the&#10;overall system information.&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt; The lexical states we are concerned&#10;with are:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;notes: Where we can actually enter music&lt;/li&gt;&#10;&lt;li&gt;chords: Used for entering music in&#10;&lt;a class="link" href="https://lilypond.org/doc/v2.26/Documentation/notation/chord-mode" target="_blank" rel="noopener"&#10; &gt;chord mode&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;figures: Used for entering&#10;&lt;a class="link" href="https://lilypond.org/doc/v2.23/Documentation/notation/figured-bass.html#introduction-to-figured-bass" target="_blank" rel="noopener"&#10; &gt;figured bass&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;lyrics: You guessed it, used for entering lyrics&lt;/li&gt;&#10;&lt;li&gt;markup: The markup system is used for entering text and symbols that are not&#10;in any other context. Primarily though, this is used for generating text such&#10;as instrumentation notes or music direction&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;For example, if we lexed a symbol/identifier &lt;code&gt;c&lt;/code&gt;:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;In a notes context, it would be treated as the pitch of a note&lt;/li&gt;&#10;&lt;li&gt;In a chords context, it would be treated as a C major chord&lt;/li&gt;&#10;&lt;li&gt;In a lyrics context, it would be treated as the word &amp;ldquo;c&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;In a markup context, it would also be treated as the word &amp;ldquo;c&amp;rdquo;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="and-the-deep-state"&gt;And the deep state&#10;&lt;/h2&gt;&lt;p&gt;But the lexical states are only half of the problem. Part of the reason the&#10;lexer needs to talk to the parser so often is because the user, through the&#10;Scheme (Guile) backend, can change the environment and/or overwrite anything&#10;except for the built-in lexical keywords.&lt;/p&gt;&#10;&lt;p&gt;Let&amp;rsquo;s go back to the example of a single letter symbol. This time let&amp;rsquo;s look at&#10;the letter &lt;code&gt;b&lt;/code&gt;. You would expect that in a notes context, it would simply be the&#10;pitch &amp;ldquo;B.&amp;rdquo; Most of the time it is, but if the user switched the music language&#10;to German, it is now B-flat. &lt;code&gt;h&lt;/code&gt; would be B natural. This can also happen in the&#10;drum mode where the user can switch the drums style. This not only changes the&#10;symbols that are accepted but all of a sudden the bass drum note can be below&#10;the staff and not on the bottom space.&lt;/p&gt;&#10;&lt;p&gt;Expressions such as &lt;code&gt;\command&lt;/code&gt; present an even larger problem. Many commands are&#10;coded into the &amp;ldquo;startup&amp;rdquo; files for LilyPond and are read before the parsing of a&#10;user file begins. These commands are also not typed in any way. &lt;code&gt;\command&lt;/code&gt; could&#10;create or modify markup, it could be a musical expression, it could be a string&#10;or another scalar value. And to build on this, within the user files, the user&#10;can overwrite any of these values (by design). This means that trying to check&#10;function arguments can&amp;rsquo;t only rely on built in command signatures. They need to&#10;be read from the LilyPond initialization files and then superseded by user&#10;definitions.&lt;/p&gt;&#10;&lt;p&gt;So how do we remove the state if it is the backbone of so many different&#10;pipelines within the lexer and parser? It truly can&amp;rsquo;t be zero state as described&#10;above. But rather, maybe thinking about it as &amp;ldquo;no hidden state.&amp;rdquo; Instead of the&#10;lexer carrying modes and pitch tables around internally, I want to make lexing a&#10;pure function&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt; of the input.&lt;/p&gt;&#10;&lt;h2 id="removing-some-of-the-state"&gt;Removing some of the state?&#10;&lt;/h2&gt;&lt;p&gt;The question then becomes, &amp;ldquo;who is responsible for what?&amp;rdquo; In my design, I hope&#10;to accomplish:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;A lexer that produces output that does not change. Tokens are the same&#10;regardless of context. Not only does this have all of the benefits above, but&#10;it also allows the lexer to look at smaller chunks of code and produce the&#10;same output for that section.&lt;/li&gt;&#10;&lt;li&gt;A parser that keeps track of its own context and builds the AST from that&#10;knowledge. Even though the tokens were lexed differently with Flex and Bison,&#10;our goal is to produce an AST that is very similar (if Flex/Bison let us&#10;inspect their AST). The parser then would also take the ownership of storing&#10;commands and their signatures by using a progressive scanning method that&#10;would have placeholders for yet-to-be discovered grammars. Because of the&#10;stateless lexer, rescanning partial parts of the code is trivial.&lt;/li&gt;&#10;&lt;li&gt;Downstream consumers that drive the parser. The parser should be used for one&#10;thing: generating the AST. It would be downstream consumers, like an LSP&#10;server, that would follow included files, load initialization files, and&#10;delegate other work to the parser and generate the &amp;ldquo;whole picture.&amp;rdquo;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;This overall design will, hopefully, fix some of the problems that plague the&#10;current lexer as well. One example is the trailing-context rules that exist only&#10;to avoid backup states. Rests (r, R), skips, and chord repetitions (q) are music&#10;events that can take post-events like &lt;code&gt;-.&lt;/code&gt;. Because of this, the lexer needs&#10;special &lt;code&gt;/[-\_]&lt;/code&gt; patterns to tell the event apart from what follows. It isn&amp;rsquo;t a&#10;bug. This is rather a shortcoming of trying to lex the syntax with regular&#10;expressions.&lt;/p&gt;&#10;&lt;figure class="code-block"&gt;&#10; &lt;div class="highlight" caption="lexer.ll (d304582e)"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-cpp" data-lang="cpp"&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;382&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="cm"&gt;/* Flex picks the longest matching pattern including trailing&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;383&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="cm"&gt; * contexts. Without the backup pattern, r-. does not trigger the&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;384&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="cm"&gt; * {RESTNAME} rule but rather the {SYMBOL}/[-_] rule coming later,&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;385&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="cm"&gt; * needed for avoiding backup states.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;386&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="cm"&gt;*/&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#10; &lt;figcaption&gt;lexer.ll (d304582e)&lt;/figcaption&gt;&#10;&lt;/figure&gt;&#10;&#10;&lt;p&gt;Some rules are extremely long and complex that don&amp;rsquo;t need to be.&lt;/p&gt;&#10;&lt;figure class="code-block"&gt;&#10; &lt;div class="highlight" caption="lexer.ll (d304582e)"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-cpp" data-lang="cpp"&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;601&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;^|*&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="err"&gt;$#&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="se"&gt;\t\n\r\f&lt;/span&gt;&lt;span class="s"&gt;0-9][^$#{}&lt;/span&gt;&lt;span class="se"&gt;\&amp;#34;\\&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="se"&gt;\t\n\r\f&lt;/span&gt;&lt;span class="s"&gt;0-9]* {&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;602&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="cm"&gt;/* ugr. This sux. */&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;603&lt;/span&gt;&lt;span class="cl"&gt;&#9;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;s&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;YYText_utf8&lt;/span&gt; &lt;span class="p"&gt;());&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;604&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;yylval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SCM_UNSPECIFIED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;605&lt;/span&gt;&lt;span class="cl"&gt;&#9;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;__&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;606&lt;/span&gt;&lt;span class="cl"&gt;&#9;&#9;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;EXTENDER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;607&lt;/span&gt;&lt;span class="cl"&gt;&#9;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;--&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;608&lt;/span&gt;&lt;span class="cl"&gt;&#9;&#9;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;HYPHEN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;609&lt;/span&gt;&lt;span class="cl"&gt;&#9;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lyric_fudge&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;610&lt;/span&gt;&lt;span class="cl"&gt;&#9;&lt;span class="n"&gt;yylval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;to_scm&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;611&lt;/span&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;612&lt;/span&gt;&lt;span class="cl"&gt;&#9;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;SYMBOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;613&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#10; &lt;figcaption&gt;lexer.ll (d304582e)&lt;/figcaption&gt;&#10;&lt;/figure&gt;&#10;&#10;&lt;p&gt;Other functions in &lt;code&gt;lexer.ll&lt;/code&gt; seem out of place as well. &lt;code&gt;YYText_utf8()&lt;/code&gt; is a&#10;function in the lexer that is called many times and analyzes &lt;em&gt;every&lt;/em&gt; byte to&#10;make sure that the utf-8 is valid. &lt;code&gt;lyric_fudge()&lt;/code&gt; exists only to replace &lt;code&gt;_&lt;/code&gt;&#10;with spaces in a loop as opposed to using a find and replace method. Overall,&#10;the goal is a system that doesn&amp;rsquo;t need comments like &amp;ldquo;Shut up lexer&#10;warnings.&amp;rdquo;&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&#10;&lt;h2 id="the-scheme-question"&gt;The Scheme question&#10;&lt;/h2&gt;&lt;p&gt;The part that is the largest unknown for me, and one that I have not quite&#10;figured out yet, is what place does the Scheme interpreter play in the lexer and&#10;parser and when does it need to be introduced. At a minimum, it needs to be&#10;parsed. But since this is a library that is planned to be used for static&#10;analysis, I don&amp;rsquo;t know if it ever needs to be evaluated. To keep the design&#10;outlined above, nothing beyond parsing should happen in the lexer. LilyPond code&#10;can also appear in Scheme code which turns this whole implementation into a&#10;turducken at best.&lt;/p&gt;&#10;&lt;h2 id="where-were-going-first"&gt;Where we&amp;rsquo;re going first&#10;&lt;/h2&gt;&lt;p&gt;To sum it all up, the Flex lexer is managing many different states. There are&#10;the lexical modes (notes, chords, figures, lyrics, markup) and there&amp;rsquo;s the&#10;environment (the pitch tables and user definitions that decide what a token even&#10;is, etc.). Neither can be deleted. The goal is a scanner with no hidden state:&#10;the parser owns the modes and the environment, and lexing becomes a pure&#10;function of the input plus the context handed to it. Downstream consumers, like&#10;our eventual LSP, drive the parser, load what needs loading, and feed it all&#10;back in. In the next part, I&amp;rsquo;ll start designing the scanner itself. It&amp;rsquo;s going&#10;to be a lot of Rust pattern matching.&lt;/p&gt;&#10;&lt;div class="footnotes" role="doc-endnotes"&gt;&#10;&lt;hr&gt;&#10;&lt;ol&gt;&#10;&lt;li id="fn:1"&gt;&#10;&lt;p&gt;These include version number parsing, including other files,&#10;parsing strings (&lt;code&gt;quote&lt;/code&gt; and &lt;code&gt;commandquote&lt;/code&gt;) and internal&#10;commands such as naming source file name and line.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:2"&gt;&#10;&lt;p&gt;&amp;ldquo;Pure function&amp;rdquo; in a programming sense. The same input always produces&#10;the same output, with no side effects.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:3"&gt;&#10;&lt;p&gt;&lt;a class="link" href="https://gitlab.com/lilypond/lilypond/-/blob/master/lily/lexer.ll#L1324" target="_blank" rel="noopener"&#10; &gt;&lt;code&gt;lexer.ll:1324&lt;/code&gt;&lt;/a&gt;&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;/div&gt;&#10;</description></item></channel></rss>