<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <title>crumbles.blog</title>
    <link href="https://crumbles.blog/atom.xml" rel="self" />
    <link href="https://crumbles.blog" />
    <id>https://crumbles.blog/atom.xml</id>
    <author>
        <name>Daphne Preston-Kendal</name>
        
        <email>dpk+ifoundthisemailillegally@nonceword.org</email>
        
    </author>
    <updated>2026-03-13T11:01:29Z</updated>
    <entry>
    <title>HOWTO: Unlock LUKS encrypted disks over SSH on a Raspberry Pi 4 running NixOS</title>
    <link href="https://crumbles.blog/posts/2026-03-13-raspberry-pi-4-luks-ssh-unlock-nixos.html" />
    <id>https://crumbles.blog/posts/2026-03-13-raspberry-pi-4-luks-ssh-unlock-nixos.html</id>
    <published>2026-03-13T12:01:29+0100</published>
    <updated>2026-03-13T11:01:29Z</updated>
    <summary type="html"><![CDATA[<p>Follow <a href="https://wiki.nixos.org/wiki/Remote_disk_unlocking">the instructions on the NixOS wiki</a>. For a Raspberry Pi 4 connected over Ethernet, you need:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a>boot<span class="op">.</span>initrd<span class="op">.</span>availableKernelModules = <span class="op">[</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>  <span class="st">&quot;xhci_pci&quot;</span></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>  <span class="st">&quot;usbhid&quot;</span></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>  <span class="st">&quot;uas&quot;</span></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a>  <span class="st">&quot;pcie-brcmstb&quot;</span></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a>  <span class="st">&quot;reset-raspberrypi&quot;</span></span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a>  <span class="st">&quot;genet&quot;</span></span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a>  <span class="st">&quot;broadcom&quot;</span></span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a>  <span class="st">&quot;bcm_phy_lib&quot;</span></span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a><span class="op">]</span>;</span></code></pre></div>
<p>Also note: use cipher <code>xchacha20,aes-adiantum-plain64</code> on Raspberry Pi 4 due to the lack of AES hardware instructions. The default <code>aes-xts-plain64</code> is slow without these instructions; <code>xchacha20,aes-adiantum-plain64</code> is over twice as fast. (Raspberry Pi 5 has AES instructions, but doesn’t support NixOS very well yet.) If you forget to set the cipher when creating the encrypted device, <a href="https://man7.org/linux/man-pages/man8/cryptsetup-reencrypt.8.html"><code>cryptsetup reencrypt</code></a> can help, but it may take multiple days once you have any real amount of data on the disk at all.</p>]]></summary>
</entry>
<entry>
    <title>HOWTO: Set up an IPsec VPN on NixOS and connect to it with Mac OS and iOS</title>
    <link href="https://crumbles.blog/posts/2026-02-07-ipsec-vpn-nixos.html" />
    <id>https://crumbles.blog/posts/2026-02-07-ipsec-vpn-nixos.html</id>
    <published>2026-02-07T16:45:13Z</published>
    <updated>2026-02-07T16:45:13Z</updated>
    <summary type="html"><![CDATA[<p>There are two options for IPsec VPNs on NixOS: Libreswan and Strongswan. Since Strongswan has much better NixOS configuration, we’ll use that.</p>
<p><strong>Note!</strong> In the best tradition of howto guides on blogs, I’m not an expert on IPsec, Strongswan, VPN configuration, nor even really NixOS. However, these are the settings that worked for me, derived mostly from the <a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-an-ikev2-vpn-server-with-strongswan-on-ubuntu-20-04">DigitalOcean guide to setting up Strongswan on Ubuntu</a> and adapted for Nix. Please try every other support forum you can think of before asking me personally for help, because I probably have no idea :-)</p>
<h2 id="generating-the-needed-certificates-etc.">Generating the needed certificates, etc.</h2>
<p>For its own very special reasons, Strongswan’s command-line tools for generating keys and certificates don’t work without an <code>/etc/strongswan.conf</code> file present, even though they don’t need it. Fortunately, an empty one is okay, so let’s create one:</p>
<pre><code>$ sudo touch /etc/strongswan.conf</code></pre>
<p>Now use the Strongswan command line tools in a Nix shell to generate the necessary credentials. (Use <code>nix-shell -p strongswan</code> if you don’t have flakes and <code>nix-command</code> enabled!)</p>
<pre><code>$ nix shell nixpkgs#strongswan</code></pre>
<p>Create directory to hold our keys and certificates and set some permissions for safety:</p>
<pre><code>$ mkdir -p ~/pki/{cacerts,certs,private}
$ chmod 700 ~/pki
$ pki --gen --type rsa --size 4096 --outform pem &gt; ~/pki/private/ca-key.pem
$ pki --self --ca --lifetime 3650 --in ~/pki/private/ca-key.pem --type rsa --dn &quot;CN=VPN root CA&quot; --outform pem &gt; ~/pki/cacerts/ca-cert.pem
$ pki --gen --type rsa --size 4096 --outform pem &gt; ~/pki/private/server-key.pem</code></pre>
<p>For a private VPN, I suggest connecting only through its IP address rather than messing about with DNS. In the following, replace <code>12.34.56.78</code> with the IP of your own server. If you really want to connect through a domain name, you can delete the last <code>--san @12.34.56.78</code> below and just use the other two with the IP address replaced by your domain name.</p>
<pre><code>$ pki --pub --in ~/pki/private/server-key.pem --type rsa \
    | pki --issue --lifetime 1825 \
          --cacert ~/pki/cacerts/ca-cert.pem \
          --cakey ~/pki/private/ca-key.pem \
          --dn &quot;CN=12.34.56.78&quot; --san 12.34.56.78 --san @12.34.56.78 \
          --flag serverAuth --flag ikeIntermediate --outform pem \
        &gt; ~/pki/certs/server-cert.pem
</code></pre>
<p>Now you can exit the Nix shell and copy these new keys and certificates to the right place:</p>
<pre><code>$ sudo cp -r ~/pki/* /etc/ipsec.d/</code></pre>
<p>Finally, delete the temporary <code>strongswan.conf</code> file we created; NixOS will manage all further Strongswan configuration.</p>
<pre><code>$ sudo rm /etc/strongswan.conf</code></pre>
<h2 id="configuring-strongswan-in-configuration.nix">Configuring Strongswan in <code>configuration.nix</code></h2>
<div class="sourceCode" id="cb7"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a>services<span class="op">.</span>strongswan = <span class="op">{</span></span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a>  <span class="va">enable</span> <span class="op">=</span> <span class="cn">true</span><span class="op">;</span></span>
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a>  <span class="co"># Where your user authentication information will be stored:</span></span>
<span id="cb7-4"><a href="#cb7-4" aria-hidden="true" tabindex="-1"></a>  <span class="va">secrets</span> <span class="op">=</span> <span class="op">[</span> <span class="st">&quot;/etc/ipsec.d/ipsec.secrets&quot;</span> <span class="op">];</span></span>
<span id="cb7-5"><a href="#cb7-5" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-6"><a href="#cb7-6" aria-hidden="true" tabindex="-1"></a>  <span class="va">setup</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb7-7"><a href="#cb7-7" aria-hidden="true" tabindex="-1"></a>    <span class="co"># Log daemon statuses</span></span>
<span id="cb7-8"><a href="#cb7-8" aria-hidden="true" tabindex="-1"></a>    <span class="va">charondebug</span> <span class="op">=</span> <span class="st">&quot;ike 1, knl 1, cfg 0&quot;</span><span class="op">;</span></span>
<span id="cb7-9"><a href="#cb7-9" aria-hidden="true" tabindex="-1"></a>    <span class="co"># Allow multiple connections</span></span>
<span id="cb7-10"><a href="#cb7-10" aria-hidden="true" tabindex="-1"></a>    <span class="va">uniqueids</span> <span class="op">=</span> <span class="st">&quot;no&quot;</span><span class="op">;</span></span>
<span id="cb7-11"><a href="#cb7-11" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb7-12"><a href="#cb7-12" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-13"><a href="#cb7-13" aria-hidden="true" tabindex="-1"></a>  <span class="va">connections</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb7-14"><a href="#cb7-14" aria-hidden="true" tabindex="-1"></a>    <span class="co"># You can change the name of the connection from `vpn` if you</span></span>
<span id="cb7-15"><a href="#cb7-15" aria-hidden="true" tabindex="-1"></a>    <span class="co"># like; this is only used internally</span></span>
<span id="cb7-16"><a href="#cb7-16" aria-hidden="true" tabindex="-1"></a>    <span class="va">vpn</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb7-17"><a href="#cb7-17" aria-hidden="true" tabindex="-1"></a>      <span class="va">auto</span> <span class="op">=</span> <span class="st">&quot;add&quot;</span><span class="op">;</span></span>
<span id="cb7-18"><a href="#cb7-18" aria-hidden="true" tabindex="-1"></a>      <span class="va">compress</span> <span class="op">=</span> <span class="st">&quot;no&quot;</span><span class="op">;</span></span>
<span id="cb7-19"><a href="#cb7-19" aria-hidden="true" tabindex="-1"></a>      <span class="va">type</span> <span class="op">=</span> <span class="st">&quot;tunnel&quot;</span><span class="op">;</span></span>
<span id="cb7-20"><a href="#cb7-20" aria-hidden="true" tabindex="-1"></a>      <span class="va">keyexchange</span> <span class="op">=</span> <span class="st">&quot;ikev2&quot;</span><span class="op">;</span></span>
<span id="cb7-21"><a href="#cb7-21" aria-hidden="true" tabindex="-1"></a>      <span class="va">fragmentation</span> <span class="op">=</span> <span class="st">&quot;yes&quot;</span><span class="op">;</span></span>
<span id="cb7-22"><a href="#cb7-22" aria-hidden="true" tabindex="-1"></a>      <span class="va">forceencaps</span> <span class="op">=</span> <span class="st">&quot;yes&quot;</span><span class="op">;</span></span>
<span id="cb7-23"><a href="#cb7-23" aria-hidden="true" tabindex="-1"></a>      <span class="co"># Detect and clear any hung connections</span></span>
<span id="cb7-24"><a href="#cb7-24" aria-hidden="true" tabindex="-1"></a>      <span class="va">dpdaction</span> <span class="op">=</span> <span class="st">&quot;clear&quot;</span><span class="op">;</span></span>
<span id="cb7-25"><a href="#cb7-25" aria-hidden="true" tabindex="-1"></a>      <span class="va">dpddelay</span> <span class="op">=</span> <span class="st">&quot;300s&quot;</span><span class="op">;</span></span>
<span id="cb7-26"><a href="#cb7-26" aria-hidden="true" tabindex="-1"></a>      <span class="va">send_cert</span> <span class="op">=</span> <span class="st">&quot;always&quot;</span><span class="op">;</span></span>
<span id="cb7-27"><a href="#cb7-27" aria-hidden="true" tabindex="-1"></a>      <span class="va">rekey</span> <span class="op">=</span> <span class="st">&quot;no&quot;</span><span class="op">;</span></span>
<span id="cb7-28"><a href="#cb7-28" aria-hidden="true" tabindex="-1"></a>      <span class="co"># Accept connections on any local network interface</span></span>
<span id="cb7-29"><a href="#cb7-29" aria-hidden="true" tabindex="-1"></a>      <span class="va">left</span> <span class="op">=</span> <span class="st">&quot;%any&quot;</span><span class="op">;</span></span>
<span id="cb7-30"><a href="#cb7-30" aria-hidden="true" tabindex="-1"></a>      <span class="co"># Set this to your domain name (prefixed with @) or your IP address</span></span>
<span id="cb7-31"><a href="#cb7-31" aria-hidden="true" tabindex="-1"></a>      <span class="va">leftid</span> <span class="op">=</span> <span class="st">&quot;12.34.56.78&quot;</span><span class="op">;</span></span>
<span id="cb7-32"><a href="#cb7-32" aria-hidden="true" tabindex="-1"></a>      <span class="va">leftcert</span> <span class="op">=</span> <span class="st">&quot;server-cert.pem&quot;</span><span class="op">;</span></span>
<span id="cb7-33"><a href="#cb7-33" aria-hidden="true" tabindex="-1"></a>      <span class="va">leftsendcert</span> <span class="op">=</span> <span class="st">&quot;always&quot;</span><span class="op">;</span></span>
<span id="cb7-34"><a href="#cb7-34" aria-hidden="true" tabindex="-1"></a>      <span class="co"># Tell clients to use this VPN connection for connections to</span></span>
<span id="cb7-35"><a href="#cb7-35" aria-hidden="true" tabindex="-1"></a>      <span class="co"># all other IP addresses</span></span>
<span id="cb7-36"><a href="#cb7-36" aria-hidden="true" tabindex="-1"></a>      <span class="va">leftsubnet</span> <span class="op">=</span> <span class="st">&quot;0.0.0.0/0&quot;</span><span class="op">;</span></span>
<span id="cb7-37"><a href="#cb7-37" aria-hidden="true" tabindex="-1"></a>      <span class="co"># Accept connection from any remote client</span></span>
<span id="cb7-38"><a href="#cb7-38" aria-hidden="true" tabindex="-1"></a>      <span class="va">right</span> <span class="op">=</span> <span class="st">&quot;%any&quot;</span><span class="op">;</span></span>
<span id="cb7-39"><a href="#cb7-39" aria-hidden="true" tabindex="-1"></a>      <span class="co"># Accept connection from any remote client ID</span></span>
<span id="cb7-40"><a href="#cb7-40" aria-hidden="true" tabindex="-1"></a>      <span class="va">rightid</span> <span class="op">=</span> <span class="st">&quot;%any&quot;</span><span class="op">;</span></span>
<span id="cb7-41"><a href="#cb7-41" aria-hidden="true" tabindex="-1"></a>      <span class="co"># Authentication method `eap-mschap-v2` works on Mac OS, iOS,</span></span>
<span id="cb7-42"><a href="#cb7-42" aria-hidden="true" tabindex="-1"></a>      <span class="co"># and allegedly on Android and Windows too</span></span>
<span id="cb7-43"><a href="#cb7-43" aria-hidden="true" tabindex="-1"></a>      <span class="va">rightauth</span> <span class="op">=</span> <span class="st">&quot;eap-mschapv2&quot;</span><span class="op">;</span></span>
<span id="cb7-44"><a href="#cb7-44" aria-hidden="true" tabindex="-1"></a>      <span class="co"># Give clients local IP addresses in the 10.0.0.0/1 subnet</span></span>
<span id="cb7-45"><a href="#cb7-45" aria-hidden="true" tabindex="-1"></a>      <span class="va">rightsourceip</span> <span class="op">=</span> <span class="st">&quot;10.0.0.0/24&quot;</span><span class="op">;</span></span>
<span id="cb7-46"><a href="#cb7-46" aria-hidden="true" tabindex="-1"></a>      <span class="co"># Set this to your preferred DNS server</span></span>
<span id="cb7-47"><a href="#cb7-47" aria-hidden="true" tabindex="-1"></a>      <span class="va">rightdns</span> <span class="op">=</span> <span class="st">&quot;1.1.1.1&quot;</span><span class="op">;</span></span>
<span id="cb7-48"><a href="#cb7-48" aria-hidden="true" tabindex="-1"></a>      <span class="co"># Clients do not need to send certificates</span></span>
<span id="cb7-49"><a href="#cb7-49" aria-hidden="true" tabindex="-1"></a>      <span class="va">rightsendcert</span> <span class="op">=</span> <span class="st">&quot;never&quot;</span><span class="op">;</span></span>
<span id="cb7-50"><a href="#cb7-50" aria-hidden="true" tabindex="-1"></a>      <span class="co"># Ask clients for identification when they connect</span></span>
<span id="cb7-51"><a href="#cb7-51" aria-hidden="true" tabindex="-1"></a>      <span class="va">eap_identity</span><span class="op">=</span><span class="st">&quot;%identity&quot;</span><span class="op">;</span></span>
<span id="cb7-52"><a href="#cb7-52" aria-hidden="true" tabindex="-1"></a>      <span class="co"># Recommended ciphersuite settings for iOS and Mac; you may need</span></span>
<span id="cb7-53"><a href="#cb7-53" aria-hidden="true" tabindex="-1"></a>      <span class="co"># different ones on other platforms</span></span>
<span id="cb7-54"><a href="#cb7-54" aria-hidden="true" tabindex="-1"></a>      <span class="va">esp</span> <span class="op">=</span> <span class="st">&quot;aes256-sha256-modp2048&quot;</span><span class="op">;</span></span>
<span id="cb7-55"><a href="#cb7-55" aria-hidden="true" tabindex="-1"></a>      <span class="va">ike</span> <span class="op">=</span> <span class="st">&quot;aes256-sha256-modp2048-modpnone&quot;</span><span class="op">;</span></span>
<span id="cb7-56"><a href="#cb7-56" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb7-57"><a href="#cb7-57" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb7-58"><a href="#cb7-58" aria-hidden="true" tabindex="-1"></a><span class="op">}</span>;</span></code></pre></div>
<p>We also need to configure the kernel to allow IP forwarding and do some related hardening by setting the appropriate sysctls:</p>
<div class="sourceCode" id="cb8"><pre class="sourceCode nix continue"><code class="sourceCode nix"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a>boot<span class="op">.</span>kernel<span class="op">.</span>sysctl<span class="op">.</span><span class="st">&quot;net.ipv4.ip_forward&quot;</span> = <span class="dv">1</span>;</span>
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a>boot<span class="op">.</span>kernel<span class="op">.</span>sysctl<span class="op">.</span><span class="st">&quot;net.ipv6.all.forwarding&quot;</span> = <span class="dv">1</span>;</span>
<span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a>boot<span class="op">.</span>kernel<span class="op">.</span>sysctl<span class="op">.</span><span class="st">&quot;net.ipv4.ip_no_pmtu_disc&quot;</span> = <span class="dv">1</span>;</span>
<span id="cb8-4"><a href="#cb8-4" aria-hidden="true" tabindex="-1"></a>boot<span class="op">.</span>kernel<span class="op">.</span>sysctl<span class="op">.</span><span class="st">&quot;net.ipv4.conf.all.accept_redirects&quot;</span> = <span class="dv">0</span>;</span>
<span id="cb8-5"><a href="#cb8-5" aria-hidden="true" tabindex="-1"></a>boot<span class="op">.</span>kernel<span class="op">.</span>sysctl<span class="op">.</span><span class="st">&quot;net.ipv4.conf.all.send_redirects&quot;</span> = <span class="dv">0</span>;</span>
<span id="cb8-6"><a href="#cb8-6" aria-hidden="true" tabindex="-1"></a>boot<span class="op">.</span>kernel<span class="op">.</span>sysctl<span class="op">.</span><span class="st">&quot;net.ipv6.conf.all.accept_redirects&quot;</span> = <span class="dv">0</span>;</span>
<span id="cb8-7"><a href="#cb8-7" aria-hidden="true" tabindex="-1"></a>boot<span class="op">.</span>kernel<span class="op">.</span>sysctl<span class="op">.</span><span class="st">&quot;net.ipv6.conf.all.send_redirects&quot;</span> = <span class="dv">0</span>;</span></code></pre></div>
<p>Finally, we need to configure the NixOS firewall to allow connections on the IPsec ports, and also to route connections through the VPN properly. (Thanks to <a href="https://wiki.strongswan.org/issues/2675">Erik Dombi on the Strongswan issue tracker</a> for the information on how to set this up.)</p>
<p>You will need to know the name of your network interface. If you don’t use a declarative, static configuration of your IP address (which for a VPN server you probably should, unless you are using Dynamic DNS or something) you may not know it. Find it with <code>ip route</code>; the network interface name is the word that appears after <code>dev</code>. (In my case, it says <code>default via 98.76.54.32 dev ens3 proto static</code>, so my interface is <code>ens3</code>.) Here I’m using <code>ens3</code>. Replace <code>ens3</code> everywhere in the <code>extraCommands</code> configuration with the name of your own interface if it’s different for you.</p>
<div class="sourceCode" id="cb9"><pre class="sourceCode nix continue"><code class="sourceCode nix"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a><span class="co"># UDP ports 500 and 4500 are used for IPsec connections</span></span>
<span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a>networking<span class="op">.</span>firewall<span class="op">.</span>allowedUDPPorts = <span class="op">[</span> <span class="dv">500</span> <span class="dv">4500</span> <span class="op">]</span>;</span>
<span id="cb9-3"><a href="#cb9-3" aria-hidden="true" tabindex="-1"></a>networking<span class="op">.</span>firewall<span class="op">.</span>extraCommands =</span>
<span id="cb9-4"><a href="#cb9-4" aria-hidden="true" tabindex="-1"></a>  <span class="st">&#39;&#39;</span></span>
<span id="cb9-5"><a href="#cb9-5" aria-hidden="true" tabindex="-1"></a><span class="st">    iptables -P INPUT ACCEPT</span></span>
<span id="cb9-6"><a href="#cb9-6" aria-hidden="true" tabindex="-1"></a><span class="st">    iptables -P FORWARD ACCEPT</span></span>
<span id="cb9-7"><a href="#cb9-7" aria-hidden="true" tabindex="-1"></a><span class="st">    iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT</span></span>
<span id="cb9-8"><a href="#cb9-8" aria-hidden="true" tabindex="-1"></a><span class="st">    iptables -A FORWARD --match policy --pol ipsec --dir in  --proto esp -s 10.0.0.0/24 -j ACCEPT</span></span>
<span id="cb9-9"><a href="#cb9-9" aria-hidden="true" tabindex="-1"></a><span class="st">    iptables -A FORWARD --match policy --pol ipsec --dir out --proto esp -d 10.0.0.0/24 -j ACCEPT</span></span>
<span id="cb9-10"><a href="#cb9-10" aria-hidden="true" tabindex="-1"></a><span class="st">    iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o ens3 -m policy --pol ipsec --dir out -j ACCEPT</span></span>
<span id="cb9-11"><a href="#cb9-11" aria-hidden="true" tabindex="-1"></a><span class="st">    iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o ens3 -j MASQUERADE</span></span>
<span id="cb9-12"><a href="#cb9-12" aria-hidden="true" tabindex="-1"></a><span class="st">    iptables -t mangle -A FORWARD --match policy --pol ipsec --dir in -s 10.0.0.0/24 -o ens3 -p tcp -m tcp --tcp-flags SYN,RST SYN -m tcpmss --mss 1361:1536 -j TCPMSS --set-mss 1360</span></span>
<span id="cb9-13"><a href="#cb9-13" aria-hidden="true" tabindex="-1"></a><span class="st">    iptables -A INPUT -j DROP</span></span>
<span id="cb9-14"><a href="#cb9-14" aria-hidden="true" tabindex="-1"></a><span class="st">    iptables -A FORWARD -j DROP</span></span>
<span id="cb9-15"><a href="#cb9-15" aria-hidden="true" tabindex="-1"></a><span class="st">  &#39;&#39;</span>;</span></code></pre></div>
<h2 id="setting-up-users">Setting up users</h2>
<p>Above in <code>configuration.nix</code> we told the Strongswan NixOS module that our secrets will come from the file <code>/etc/ipsec.d/ipsec.secrets</code>, so we need to create it, tell it where to find our server private key, and add some users:</p>
<pre><code>: RSA &quot;server-key.pem&quot;
username : EAP &quot;password&quot;</code></pre>
<p>Replace <code>username</code> and <code>password</code> by your chosen credentials. You can add more lines of this type if you want more users.</p>
<h2 id="run-it">Run it!</h2>
<p><code>nixos-rebuild</code> will start the Strongswan service, reconfigure the firewall, and you should have a working VPN.</p>
<p>For some reason after a system reboot, the VPN seems to take a minute or two to start; <code>nixos-rebuild</code> manages to bring it up immediately.</p>
<h2 id="connecting-on-mac-os-and-ios">Connecting on Mac OS and iOS</h2>
<p>The advantage of an IPsec VPN is that it’s supported natively by Mac OS and iOS. The process is pretty similar on both:</p>
<ol type="1">
<li><p>Download the <code>ca-cert.pem</code> file from <code>/etc/ipsec.d/cacerts</code></p></li>
<li><p>On Mac, open it in Keychain Access and add it to the System keychain. You then need to open the System keychain, right click on the certificate and choose ‘Get Info’, unfold the ‘Trust’ disclosure triangle, and select at least ‘Always Trust’ for IPsec.</p>
<p>On an iOS device, after downloading the file, you will be offered to install the new ‘profile’ by going to the Settings app. Once you install it, you still need to mark the certificate as trusted. Go to General → About → Certificate Trust Settings (right at the bottom) and enable full trust for that root certificate. (There is no fine-grained trust setting on iOS, at least for manual configuration, as far as I know.)</p></li>
<li><p>Add a VPN in the VPN settings pane, choosing IKEv2 as the type. Set both the ‘Server address’ and ‘Remote ID’ to the IP address or domain name of your VPN server. Under ‘Authentication’, select ‘User authentication’ method ‘Username’ and enter the username and password you put in the <code>/etc/ipsec.d/ipsec.secrets</code> file.</p></li>
<li><p>Create the VPN and turn it on. Hopefully it will work!</p></li>
</ol>
<h2 id="improvements-i-want-to-make-in-a-future-version-of-this-guide">Improvements I want to make in a future version of this guide</h2>
<ul>
<li>Use Agenix to store secrets instead of just dumping them in <code>/etc</code></li>
<li>Understand more about what those <code>iptables</code> incantations do and maybe pare them down</li>
</ul>]]></summary>
</entry>
<entry>
    <title>Make the font bigger</title>
    <link href="https://crumbles.blog/posts/2025-12-11-presentation-fonts.html" />
    <id>https://crumbles.blog/posts/2025-12-11-presentation-fonts.html</id>
    <published>2025-12-11T10:16:55Z</published>
    <updated>2025-12-11T10:16:55Z</updated>
    <summary type="html"><![CDATA[<p>Am I the only one who often gets obsessed with stuff they discovered in really weird ways? I watch a film and some character listens to some music on their car stereo; I look it up and have a new favourite band for the next few months. That kind of thing.</p>
<p>Along those lines, quite a while ago I made an <a href="https://chaos.social/@dpk/113271193678003974">unfunny remark</a> on Mastodon about the tendency of some widely-respected computer scientists to use a certain font in their presentations; just recently, prompted by referring to that post for the nth time – so much for jokes getting funnier by repetition – I decided to look into <em>why</em> they do this, even though that particular font is widely disliked. The answer turned out to be pretty much that they don’t care, and they wish people would focus on the content instead of the font. Fair enough.</p>
<p>Except someone also mentioned that that particular font might be good to use because it’s allegedly easier to read for dyslexic people.</p>
<p>Ah. Hmm. Hrrmph. Well, maybe I need to do something about this in future as well, then!</p>
<p>Cue a whole morning spent overthinking this!</p>
<p>The first thing to do was to find out whether that claim was really true of that font in particular. Answer: unsurprisingly, <a href="https://www.reddit.com/r/Dyslexia/comments/jtgovc/a_psa_on_dyslexia_friendly_fonts_from_the/">initial research pointed to ‘no’</a>; some people say it helps, but just as for non-dyslexic people, opinions vary and you can’t generalize over ‘dyslexic people’ as a class here. For one thing, there are <a href="https://doi.org/10.1515/9781614514909-036">very different types of dyslexia.</a> The chap who wrote that Reddit post says it only reminded him of the patronizing treatment he got as a dyslexic kid at school; again, that’s fair enough.<a href="#fn1" class="footnote-ref" id="fnref1" role="doc-noteref"><sup>1</sup></a></p>
<p>There are a couple of fonts – <a href="https://dyslexiefont.com/en/typeface/">Dyslexie</a> and <a href="https://opendyslexic.org/">OpenDyslexic</a> – which claim to be designed to be helpful for dyslexic people. The idea of both seems to be the same: somehow making the stroke weight heavier at the bottom of the font maybe gives the letters more ‘gravity’, as if that would help them stay in place better?</p>
<p>Type designer <a href="https://about.cubictype.com/">David Jones</a> happened upon my posts fretting about this on Mastodon and was able to point me to a <a href="https://web.archive.org/web/20200621202901/https://bigelowandholmes.typepad.com/bigelow-holmes/2014/11/typography-dyslexia.html">handy introduction to the topic of legibility for dyslexic folk</a> by big-shot typographer Chuck Bigelow. Bigelow looks at the actual scientific research: on this kind of bottom-heavy font in particular, it’s a wash.</p>
<details>
<summary>
Sidebar! Personal hypothesis about why these fonts might feel like they work for some dyslexic people
</summary>
<p>Readers recognize words by their whole shape, not a letter at a time. The more common the shape, the easier it is to recognize the word. This applies for dyslexic people just as much as non-dyslexic people. For (some) dyslexic people, it’s just harder to pick apart the differences between words that have similar shapes.</p>
<p>This is why place names on road signs are in mixed case, not upper case. In the UK, for example, they used to be in upper case, with the idea that because upper case is bigger it would be easier to read from further away. But people are more used to seeing place names written in running text in mixed case, so the shape is easier to recognize in mixed case. So <a href="https://en.wikipedia.org/wiki/Worboys_Committee">in 1963</a> it was fixed to require ordinary mixed case on road signs.</p>
<p>My guess as to why the dyslexic fonts might work for some people is just that they have such unusual letterforms compared to normal fonts that they make the shapes of words unfamiliar enough as to force word recognition to fall back to a lower level. In the same way that non-dyslexic readers have to fall back on letter-at-a-time (or morpheme-at-a-time) recognition to deal with unfamiliar words like quixolotl (a quixotic axolotl) or all-caps words like ASTROLABE, these fonts make everyone fall back to engaging more with the shapes of individual letters, reducing confusions between similar-looking words.</p>
<p>The real problem with this is that it also implies that as dyslexic readers get more used to OpenDyslexia/Dyslexie, the more they’ll get used to identifying words by shape in them, and the more they’ll go back to struggling to tell similarly-shaped words apart.</p>
<p>This is just an educated guess. It would be interesting to come up with an experiment to see if it could be tested.</p>
</details>
<p>To me, promoting these fonts now feels like a kind of inclusionist techno-solutionism all too common on the hackerly left, like the able-bodied Mastodon HOA insisting on <a href="https://www.jwz.org/blog/2025/02/on-blocking-mastodon-hoa-edition/">alt texts that actually make life <em>worse</em> for those who depend on assistive technologies</a>. With this one weird trick, you too can help people overcome their disability! Unfortunately, inclusion and accessibility are hard (though that <em>certainly</em> doesn’t mean they’re not worthwhile!) and there <em>isn’t</em> one weird trick to include everyone without accidentally disadvantaging some. If there is something you can do to try to include more people, it needs careful thought and consideration about the best way to make that affordance available, and about how to avoid accidentally hindering more people than you help – or worse, hindering the people you’re actually trying to help. In this case, for individual dyslexic people who feel like these fonts actually do help them read, and know where to find the setting to make websites display in those fonts, sure;<a href="#fn2" class="footnote-ref" id="fnref2" role="doc-noteref"><sup>2</sup></a> but for a general audience, maybe not – we need a different solution here.</p>
<p>Bigelow <em>does</em> point to some clear results saying there’s a change you can make that makes readability measurably better for dyslexic people: <a href="https://pmc.ncbi.nlm.nih.gov/articles/PMC1427019/">make the font bigger!</a></p>
<p>That study has some limitations: the sample size was small and included only children, but it seems it was prompted by <a href="https://doi.org/10.1016/s0042-6989(00)00041-9">a previous study</a> which included adults and gave initial positive results for increased font size across age ranges, prompting further investigation by the authors, so the indications are good that this isn’t a fluke or limited to children. The other major limitation is that both studies concern passively lit running text at close distance, not bullet points projected on a wall on the opposite side of the room; intuitively that doesn’t feel like a relevant factor. Also, increasing the font size only works to a certain point, at which point the benefits stop increasing (but, crucially, things don’t actually get worse), which also makes intuitive sense: as a non-dyslexic person, very fine print is harder to read than normal-size print, but after a certain point it makes no difference – 6 pt type is pretty hard going, but I can read 10 pt type just as well as 72 pt type. The result of the study is specifically that this threshold seems to be about a third higher for dyslexic children.</p>
<p>Of course, bigger text on a slide means you can fit less on, but I’m of the view that <a href="https://matt.might.net/articles/academic-presentation-tips/#:~:text=paper%3F-,Slides">less text per slide is better anyway</a>. I’m still not very good at practicing what I preach on that topic, so encouragement from a larger font size probably ends up being good for me as presenter as well! And, of course, a bigger font size is probably going to be better for audience members with vision impairments too. Wins all around.</p>
<p>So I boosted the font size of the slides I was working on by about 50% where I could; slightly less in some cases. The change also prompted me to redesign or split up some otherwise overcrowded slides.</p>
<p>Bigelow points to two other positive results. One is for a shorter line length: with a fixed width projector screen, a bigger font means shorter line length anyway, so that’s an easy win as well. But also that seems more relevant for running text than for short points on a slide: if you have text that spans more than one line on a slide, that’s already suspect; three or more lines is barely even a slide any more – it’s stopped being a visual aid for speaking and turned into a copy and paste of the paper for reading.</p>
<p>The other vaguely positive result is for slightly increased letter spacing (which David Jones suggested might be the reason the font discussed at the start of this post works for some dyslexic people). The results here are less clear than for font size or line length, but choosing a familiar font with slightly looser spacing would also be a change that would be unlikely to make anything <em>worse</em> for some people. I can kind of see the problem here: at the moment I use Helvetica, and the letters ‘tt’ in the word ‘pattern’ sometimes join together at the crossbars, making them look like a single letter.</p>
<p>I looked at the fonts <a href="https://software.sil.org/andika/">Andika</a> and <a href="https://www.brailleinstitute.org/freefont/">Atkinson Hyperlegible</a> as potential alternatives that are specifically designed to avoid situations that make words difficult to read or tell apart, but I haven’t yet made up my mind about switching. Atkinson in particular seems to be aimed at helping people with vision impairment more than dyslexia – not that that would be a bad thing, but it also can’t be denied that some of its letterforms deviate from the principle of familiarity of form which seems significant in helping dyslexic people. From my anecdotal experience with my own short-sightedness (only −1.25 dioptres in both eyes, admittedly), it also feels like I probably did much, much more to help by increasing the font size than I could do by switching to a different font; from my experience, probably like an order of magnitude more. For my personal stuff in general, I’ve mostly standardized on Adrian Frutiger’s <a href="https://en.wikipedia.org/wiki/Univers">Univers</a> (in the case of this website, through the noncommercial-licensed GhostScript digitization); perhaps I should look at using that on my slides as well. It feels like it has a little more character than Helvetica, perhaps simply through being less overexposed, and it does also have slightly looser tracking.</p>
<p>Anyway, make the font bigger – for real! I think there are only wins to be had in that direction. You’ll make better, simpler slides; your vision-impaired and dyslexic audience members may thank you.</p>
<section id="footnotes" class="footnotes footnotes-end-of-document" role="doc-endnotes">
<hr />
<ol>
<li id="fn1"><p>I sympathize with unhappy school memories associated with that font – to me, it only reminds me of the handouts my school German teacher gave us. Funnily (considering where I now live), German was my worst subject in school, and the teacher was an authoritarian ass who liked to pick on weaker students, so those aren’t good memories for me either.<a href="#fnref1" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn2"><p>Incidentally, designers, have you tried out your website or app with a custom font setting recently? Have you checked that no button text is unnecessarily truncated; that no important text overflows onto a second, partially invisible line? Another benefit: if your thing is only (as yet) available in one language, making sure that everything works with a wider or narrower font is an important step to be ready for the day when a translator comes along and wants to change the <em>whole text</em>, which will have just as big an effect on the relative widths of all the links and buttons.<a href="#fnref2" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
</ol>
</section>]]></summary>
</entry>
<entry>
    <title>Tour of a pattern matcher: decision trees</title>
    <link href="https://crumbles.blog/posts/2025-11-28-extensible-match-decision-tree.html" />
    <id>https://crumbles.blog/posts/2025-11-28-extensible-match-decision-tree.html</id>
    <published>2025-11-28T14:11:49Z</published>
    <updated>2025-11-28T14:11:49Z</updated>
    <summary type="html"><![CDATA[<p>I’ve teased it long enough:<a href="#fn1" class="footnote-ref" id="fnref1" role="doc-noteref"><sup>1</sup></a> today we’re going to look at the real meat of turning declarative, something-that-looks-like-this patterns into fast, running, imperative, if-this-then-that code. The absolute <em>final</em> generation of working Scheme code will have to wait until our thrilling conclusion in part 4, but by the end of this episode you’ll probably at least have an idea of what that will look like.</p>
<p>Today we’re in the <a href="https://codeberg.org/dpk/extensible-match/src/commit/924ebeb58653ab8b77a51b45c4de29fb32f88dee/extensible-match/decision-tree.sls"><code>(extensible-match decision-tree)</code></a> library, which takes patacts (the AST representation of a pattern match clause introduced <a href="/posts/2025-11-15-extensible-match-ast.html">last time</a>) and produces an optimized decision tree. It starts with a foreboding comment:</p>
<pre><code>;; This is some of the hardest code I have ever had to write.</code></pre>
<p>And, indeed, not since I was a tweenaged baby hacker struggling through <a href="https://web.archive.org/web/20060114091344/http://www.policyalmanac.org/games/aStarTutorial.htm">an online tutorial</a> on how to implement A* pathfinding for my game<a href="#fn2" class="footnote-ref" id="fnref2" role="doc-noteref"><sup>2</sup></a> have I had so many false starts and wrong directions in implementing a known algorithm. But these were problems with working out how to translate the idea into code – the idea itself is pretty intuitive!</p>
<h2 id="motivating-example">Motivating example</h2>
<p>To see how we benefit from an optimizing decision tree generator, let’s look at an example not all too dissimilar from the <code>last</code> example we saw in the previous part.<a href="#fn3" class="footnote-ref" id="fnref3" role="doc-noteref"><sup>3</sup></a></p>
<div class="sourceCode" id="cb2"><pre class="sourceCode scheme"><code class="sourceCode scheme"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a>(<span class="ex">define</span><span class="fu"> </span>(uniq ls same?) <span class="co">; remove adjacent equivalent elements from list</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>  (match ls</span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a>    (&#39;()</span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a>     &#39;())</span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a>    ((<span class="kw">cons</span> <span class="op">_</span> &#39;())</span>
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a>     ls)</span>
<span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a>    ((<span class="kw">cons*</span> y x ls*)</span>
<span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a>     (<span class="kw">if</span> (same? y x)</span>
<span id="cb2-9"><a href="#cb2-9" aria-hidden="true" tabindex="-1"></a>         (uniq (<span class="kw">cons</span> x ls*) same?)</span>
<span id="cb2-10"><a href="#cb2-10" aria-hidden="true" tabindex="-1"></a>         (<span class="kw">cons</span> y (uniq (<span class="kw">cons</span> x ls*) same?))))))</span></code></pre></div>
<p>For this input, a typical Lisp hacker’s first attempt at a pattern matcher implementation usually generates code that looks basically like this:</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode scheme"><code class="sourceCode scheme"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a>(<span class="ex">define</span><span class="fu"> </span>(uniq ls same?)</span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a>  (<span class="kw">let</span> ((subject ls))</span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a>    (<span class="ex">define</span><span class="fu"> </span>(clause-1)</span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a>      (<span class="kw">if</span> (<span class="kw">equal?</span> subject &#39;())</span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a>          &#39;()</span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a>          (clause-2)))</span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a>    (<span class="ex">define</span><span class="fu"> </span>(clause-2)</span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a>      (<span class="kw">if</span> (<span class="kw">pair?</span> subject)</span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a>          (<span class="kw">let</span> ((g0 (<span class="kw">car</span> subject)) (g1 (<span class="kw">cdr</span> subject)))</span>
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a>            (<span class="kw">if</span> (<span class="kw">equal?</span> g1 &#39;())</span>
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a>                ls</span>
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a>                (clause-3)))</span>
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a>          (clause-3)))</span>
<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a>    (<span class="ex">define</span><span class="fu"> </span>(clause-3)</span>
<span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a>      (<span class="kw">if</span> (<span class="kw">pair?</span> subject)</span>
<span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a>          (<span class="kw">let</span> ((g2 (<span class="kw">car</span> subject))</span>
<span id="cb3-17"><a href="#cb3-17" aria-hidden="true" tabindex="-1"></a>                (g3 (<span class="kw">cdr</span> subject)))</span>
<span id="cb3-18"><a href="#cb3-18" aria-hidden="true" tabindex="-1"></a>            (<span class="kw">if</span> (<span class="kw">pair?</span> g3)</span>
<span id="cb3-19"><a href="#cb3-19" aria-hidden="true" tabindex="-1"></a>                (<span class="kw">let</span> ((g4 (<span class="kw">car</span> g3)) (g5 (<span class="kw">cdr</span> g3)))</span>
<span id="cb3-20"><a href="#cb3-20" aria-hidden="true" tabindex="-1"></a>                  (<span class="kw">let</span> ((y g2) (x g4) (ls* g5))</span>
<span id="cb3-21"><a href="#cb3-21" aria-hidden="true" tabindex="-1"></a>                    (<span class="kw">if</span> (same? y x)</span>
<span id="cb3-22"><a href="#cb3-22" aria-hidden="true" tabindex="-1"></a>                        (uniq (<span class="kw">cons</span> x ls*) same?)</span>
<span id="cb3-23"><a href="#cb3-23" aria-hidden="true" tabindex="-1"></a>                        (<span class="kw">cons</span> y (uniq (<span class="kw">cons</span> x ls*) same?)))))</span>
<span id="cb3-24"><a href="#cb3-24" aria-hidden="true" tabindex="-1"></a>                (no-matching-clause)))</span>
<span id="cb3-25"><a href="#cb3-25" aria-hidden="true" tabindex="-1"></a>          (no-matching-clause)))</span>
<span id="cb3-26"><a href="#cb3-26" aria-hidden="true" tabindex="-1"></a>    (<span class="ex">define</span><span class="fu"> </span>(no-matching-clause)</span>
<span id="cb3-27"><a href="#cb3-27" aria-hidden="true" tabindex="-1"></a>      (<span class="kw">assertion-violation</span> &#39;match <span class="st">&quot;no clause matches&quot;</span> subject))</span>
<span id="cb3-28"><a href="#cb3-28" aria-hidden="true" tabindex="-1"></a>    (clause-1)))</span></code></pre></div>
<p>The idea is simple: compile each clause to a procedure, and tail-call the next procedure’s clause as a ‘failure continuation’ at any point where you notice that the pattern doesn’t match.</p>
<p>This is a completely reasonable implementation for very many, nay, probably even a majority of use cases. It’s how everyone learning to write a pattern matcher starts out, and indeed probably how every individual pattern match compiler starts out (<a href="https://codeberg.org/dpk/extensible-match/src/commit/e09dae8db289c2a685164aae17caefad1a634ae6/extensible-match/match.scm#L171">including extensible-match</a>!) The <code>syntax-case</code> pattern matcher implemented by psyntax basically works like this, because the absolute fastest matching time isn’t a priority for something that runs at expand time. <a href="https://synthcode.com/scheme/match.scm">Alex Shinn’s pattern matcher implementation</a> works like this – anything more complicated would probably be hell to write in pure <code>syntax-rules</code>, and the sheer portability Shinn managed to achieve by writing only in <code>syntax-rules</code> has let his pattern matcher take over the world, inefficiencies and the occasional fudging on semantics be damned. (By contrast, SRFI 262’s dependence on identifier properties will likely hold it back from any equivalent level of world dominance for some time yet, until more Scheme implementers start to catch on to how nifty they are.)</p>
<p>Some of the inefficiencies in this particular code will be optimized away by the Scheme compiler. I mentioned in <a href="/posts/2025-11-09-extensible-match-front-end.html#the-match-forms">the first part of this series</a> that redundant <code>let</code> bindings are easy to get rid of. Unfortunately, more consequential inefficiencies are harder to deal with. Most significantly, even after optimization, this code will call <code>pair?</code> and <code>cdr</code> <em>twice</em> every time it goes through the loop: once for the second clause, then again in the third clause. While Scheme compilers are usually clever enough to eliminate repeated type checks when they occur within a single nested tree of code, putting each check in its own little procedure messes this compiler optimization up. The exception is Guile, because Andy Wingo developed a <a href="https://github.com/wingo/online-cse">very clever compiler pass</a> which can recognize common prefixes across these internal procedure boundaries. Also, <em>very</em> simple uses – like the <code>last</code> example from last episode – can have the clauses inlined into one another and thus end up in a nice nested tree for the compiler to work with; but as a data point, <code>clause-3</code> above is already complex enough (or complex enough and referenced from enough places) that Chez’s cp0 doesn’t bother.</p>
<p>This repeated calling and branching is theoretically unsatisfactory, but the real philosophical issue in these inefficiencies – the thing that really motivated me to want to do better – is specifically that this generated code is nothing like what a programmer would write in the absence of pattern matching. Nobody would ever write code which had those repeated <code>pair?</code> calls. The real point I want to make by spending many lines of code on trying to do something better is that <em>pattern matching is a good idiom, and programmers should use it.</em> To that end, pattern matching should be a usable tool even in the hottest of hot loops, even on the slowest of microcontrollers: nobody should ever feel like they have to switch back to writing out <code>if</code> expressions by hand for performance’s sake. The fact that pattern matching is a declarative idiom, and declarative programming has <a href="https://chaos.social/@dpk/113203052544851298">something of a reputation for being inefficient</a>, doesn’t exactly help its reputation here.</p>
<p>There are mountains of research into efficient compilation of pattern matching to avoid generating silly code like this. In the usual framing, there are two approaches: the backtracking automaton and the decision tree. The backtracking automaton, as its name implies, will still sometimes run the same tests on the same subject more than once, but tries to do so less than the naïve approach shown above which re-examines everything from scratch in every clause. The decision tree, on the other hand, will never re-examine any of its inputs, but may have much larger (exponentially larger!) code size than the automaton.</p>
<p>In fact these two approaches aren’t so different in practice – once you run the right time-saving optimizations for generating an automaton, or the right space-saving optimizations for generating a decision tree, the results are pretty close to one another. They’ll be nearly identical on very, very many use cases; it’s in the comparatively rare cases where they fall down that they fall down in different ways.</p>
<p>In the Scheme and Scheme-adjacent world, Racket’s pattern matcher by Sam Tobin-Hochstadt uses an optimized backtracking automaton after the <a href="https://pauillac.inria.fr/~maranget/papers/opat/">technique by Le Fessant and Maranget (2001)</a>;<a href="#fn4" class="footnote-ref" id="fnref4" role="doc-noteref"><sup>4</sup></a> extensible-match uses a decision tree after the <a href="http://moscova.inria.fr/~maranget/papers/ml05e-maranget.pdf">technique by Maranget (2008)</a>. Yep, <a href="https://pauillac.inria.fr/~maranget/">same guy!</a> Luc Maranget is actually far from the only compiler researcher to have written papers on optimizing both techniques; but he is the only one whose papers now seem to be regarded as the somewhat definitive introduction to the state of the art for each of the two techniques.</p>
<h2 id="generating-a-decision-tree">Generating a decision tree</h2>
<p>Maranget’s 2008 paper is heavy on notation, but mostly explains it as it goes along. It’s pretty accessible as European academia goes: if you’re good at reading and understanding other people’s code, you should be able to understand Maranget’s paper. It’s worth a read (or take a look at a shorter explanation by <a href="https://compiler.club/compiling-pattern-matching/">Colin James</a> or <a href="https://github.com/clojure/core.match/wiki/Understanding-the-algorithm">David Nolen</a>) – I’ll explain it here as it’s implemented in extensible-match, noting some (but not all!) differences to Maranget’s original presentation.</p>
<p>So, our input is a list of patterns and their associated actions, one for each clause. In fact, our input is always a list of <code>row</code>s of patterns – remember, we’re in <code>%core-match-lambda</code> which matches multiple patterns against multiple values, using the <code>row-pattern</code> AST type to group them together. Each AST pattern (and subpattern, but we’ll get to that) represents something we can do to get closer to picking a clause which matches the subject – meaning that with the <code>row-pattern</code>, we have a choice of multiple potential patterns to choose from. The <code>?-pattern</code>, for example, represents that we could test its subject against a predicate; the <code>var-pattern</code> represents renaming an internal subject variable to a pattern variable visibly bound in the clause.</p>
<p>Let’s say we pick one pattern from the first row, since that’s the one we have to test first. (How we pick one is a topic we’ll get to later.) We’re going to act on this pattern – let’s say a <code>?-pattern</code> testing <code>pair?</code> against our input <code>x</code>. What do we need to do?</p>
<p>What we ultimately want to do is generate <code>(if (pair? x) &lt;code-if-a-pair&gt; &lt;code-if-not-a-pair&gt;)</code>; this gives us a hint that we also need to work out how to fill in those two slots. Well, what goes in those two slots?</p>
<p>The <code>&lt;code-if-not-a-pair&gt;</code> clause is easier to understand first, because if <code>(pair? x)</code> is false, the whole row can’t match any more: a row is an <em>and</em> combination of patterns, and one of the <em>and</em>ed subpatterns just threw up false. So we can throw out that entire row and fill in <code>&lt;code-if-not-a-pair&gt;</code> with the code generated by repeating this process on that smaller list of rows with their actions.</p>
<p>But wait! What if the next row also contains a <code>?-pattern</code> testing <code>pair?</code> on <code>x</code> – there’s no point testing that again, because it will be false a second time as well! So apart from throwing away the first row, we also walk through the remaining rows and throw away any of them which also depend on the same test before we repeat this process to generate the <code>&lt;code-if-not-a-pair&gt;</code>.</p>
<p>The <code>&lt;code-if-a-pair&gt;</code> is generated by a similar walk over the rows and recursion on the result. We don’t throw away the first row because in this case, that part of it <em>did</em> match. Instead, we remove it from the row and from any subsequent rows which make exactly the same test, preventing generating the same test again in the case where we know it succeeded as well as the case where we know that it failed.</p>
<p>That’s basically the core of Maranget’s algorithm. Maranget calls generating the new rows for <code>&lt;code-if-a-pair&gt;</code> <dfn>specializing</dfn> the patterns, and generating the new rows for <code>&lt;code-if-not-a-pair&gt;</code> <dfn>defaulting</dfn> the patterns – both with respect to a particular ‘constructor’, but because Scheme is different (more below), extensible-match calls these specializers. There are two base cases of the recursion: one when there are no rows left (no patterns matched; generate the code to signal a matching failure) and one when the top row only consists of <dfn>irrefutable</dfn> patterns (ones like <code>_</code> wildcard patterns and <code>var-pattern</code>s, which don’t have failure cases: in this case that row matched; generate the code to activate the corresponding action).</p>
<p>However, there’s one type of pattern in our AST which doesn’t have a failure case, but hides more subpatterns which do: the <code>apply-pattern</code> which calls some procedure on its subject variable and creates more subject variable(s) for the value(s) returned. Those subject variables are then available to be the subject of a subpattern of the <code>apply-pattern</code>.</p>
<p>For his equivalent of an <code>apply-pattern</code> Maranget makes a whole separate set of patterns when specializing, just containing the subpatterns. This probably works better for his compiler of ML patterns than it did when I tried in my compiler of Scheme patterns: for one thing, testing type (our <code>?-pattern</code>) and deconstructing into values for subpatterns (our <code>apply-pattern</code>) are the same thing in his implementation; since ML is statically typed and pattern matching is the primitive means of deconstruction, the compiler has a lot more knowledge about the potential range of input values and what is and isn’t possible after each deconstruction step than extensible-match can get as a Scheme macro. In particular, Maranget’s patterns after each deconstruction step always form a matrix with columns for each value and rows for each clause. That’s not guaranteed by the structure of this AST (but explains why the term ‘row’ is used in extensible-match).</p>
<p>So instead, when an <code>apply-pattern</code> has been chosen as the specializer, each specialized row simply has the subpattern of that <code>apply-pattern</code> spliced into it. This means that each <code>row-pattern</code> can have a different length, depending on which <code>apply-pattern</code>s have been specialized and had their subpatterns spliced in!</p>
<p>As one last point, in order to simplify the job of the procedures which pick a specializer and specialize and default the rows on it, after this splicing takes place (or more accurately before each recursive step), any rows nested within the rows are flattened out. If there were any <code>or</code> patterns, they’re also expanded into multiple copies of the entire patact, each with one of the <code>or</code> clauses, so the decision tree generator proper doesn’t need to look for specializers specially inside of <code>or</code> patterns either.</p>
<h2 id="representing-a-decision-tree">Representing a decision tree</h2>
<p>For reasons that will become more apparent below and in the next episode, a decision tree is actually another kind of intermediate representation: we don’t represent it directly as Scheme code. Here’s its definition, slightly simplified:</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode scheme"><code class="sourceCode scheme"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a>(<span class="kw">define-record-type</span> dt-node</span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a>  (fields success-branch failure-branch))</span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a>(<span class="kw">define-record-type</span> dt-test</span>
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a>  (fields proc var)</span>
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a>  (parent dt-node))</span>
<span id="cb4-6"><a href="#cb4-6" aria-hidden="true" tabindex="-1"></a>(<span class="kw">define-record-type</span> dt-apply</span>
<span id="cb4-7"><a href="#cb4-7" aria-hidden="true" tabindex="-1"></a>  (fields proc var vars)</span>
<span id="cb4-8"><a href="#cb4-8" aria-hidden="true" tabindex="-1"></a>  (parent dt-node))</span>
<span id="cb4-9"><a href="#cb4-9" aria-hidden="true" tabindex="-1"></a>(<span class="kw">define-record-type</span> dt-equal</span>
<span id="cb4-10"><a href="#cb4-10" aria-hidden="true" tabindex="-1"></a>  (fields val var)</span>
<span id="cb4-11"><a href="#cb4-11" aria-hidden="true" tabindex="-1"></a>  (parent dt-node))</span>
<span id="cb4-12"><a href="#cb4-12" aria-hidden="true" tabindex="-1"></a>(<span class="kw">define-record-type</span> dt-rename</span>
<span id="cb4-13"><a href="#cb4-13" aria-hidden="true" tabindex="-1"></a>  (fields internal external)</span>
<span id="cb4-14"><a href="#cb4-14" aria-hidden="true" tabindex="-1"></a>  (parent dt-node))</span></code></pre></div>
<p>Immediately it’s clear how much simpler this is than <a href="/posts/2025-11-15-extensible-match-ast.html#the-abstract-syntax-tree-ast">the AST we got as input</a>: only four concrete node types vs nine pattern types in the AST.<a href="#fn5" class="footnote-ref" id="fnref5" role="doc-noteref"><sup>5</sup></a> The <code>action</code> type from the AST also appears in decision trees, as the leaf nodes, where a matching clause has been chosen or the match failed.</p>
<p><code>?-pattern</code>s, when chosen, become <code>dt-test</code> nodes; <code>apply-patterns</code> become <code>dt-apply</code> nodes; <code>quote-pattern</code>s become <code>dt-equal</code> nodes; <code>var-pattern</code>s become <code>dt-rename</code> nodes. <code>and-pattern</code>s, <code>or-patterns</code>, <code>row-pattern</code>s, and <code>not-pattern</code>s are reflected in the structure of the tree rather than as nodes themselves. <code>wildcard-pattern</code>s are no-ops, as far as the pattern match compiler is concerned, and don’t show up in the tree at all.</p>
<h2 id="step-by-step">Step by step</h2>
<p>Here’s a worked example. Let’s take the <code>last</code> example from the previous episode:</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode scheme"><code class="sourceCode scheme"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a>(<span class="ex">define</span><span class="fu"> </span>(last ls) <span class="co">; returns the last element in a list</span></span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a>  (match ls</span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a>    ((<span class="kw">cons</span> x &#39;())  x)</span>
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a>    ((<span class="kw">cons</span> <span class="op">_</span> ls*)  (last ls*))))</span></code></pre></div>
<p>Our AST for this pattern match expression looks like this (in an entirely hypothetical notation):</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode scheme"><code class="sourceCode scheme"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a>((patact (row (<span class="kw">and</span> (<span class="op">?</span> #:subject ls #:predicate <span class="kw">pair?</span>)</span>
<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a>                   (row (apply #:subject    ls</span>
<span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a>                               #:procedure  <span class="kw">car</span></span>
<span id="cb6-4"><a href="#cb6-4" aria-hidden="true" tabindex="-1"></a>                               #:vars       (ls.car)</span>
<span id="cb6-5"><a href="#cb6-5" aria-hidden="true" tabindex="-1"></a>                               #:subpattern (row (var #:subject ls.car</span>
<span id="cb6-6"><a href="#cb6-6" aria-hidden="true" tabindex="-1"></a>                                                      #:name x)))</span>
<span id="cb6-7"><a href="#cb6-7" aria-hidden="true" tabindex="-1"></a>                        (apply #:subject    ls</span>
<span id="cb6-8"><a href="#cb6-8" aria-hidden="true" tabindex="-1"></a>                               #:procedure  <span class="kw">cdr</span></span>
<span id="cb6-9"><a href="#cb6-9" aria-hidden="true" tabindex="-1"></a>                               #:vars       (ls.cdr)</span>
<span id="cb6-10"><a href="#cb6-10" aria-hidden="true" tabindex="-1"></a>                               #:subpattern (row (<span class="kw">quote</span> #:subject ls.cdr</span>
<span id="cb6-11"><a href="#cb6-11" aria-hidden="true" tabindex="-1"></a>                                                        #:datum ()))))))</span>
<span id="cb6-12"><a href="#cb6-12" aria-hidden="true" tabindex="-1"></a>         (action return x))</span>
<span id="cb6-13"><a href="#cb6-13" aria-hidden="true" tabindex="-1"></a> (patact (row (<span class="kw">and</span> (<span class="op">?</span> #:subject ls #:predicate <span class="kw">pair?</span>)</span>
<span id="cb6-14"><a href="#cb6-14" aria-hidden="true" tabindex="-1"></a>                   (row (apply #:subject    ls</span>
<span id="cb6-15"><a href="#cb6-15" aria-hidden="true" tabindex="-1"></a>                               #:procedure  <span class="kw">car</span></span>
<span id="cb6-16"><a href="#cb6-16" aria-hidden="true" tabindex="-1"></a>                               #:vars       (ls.car)</span>
<span id="cb6-17"><a href="#cb6-17" aria-hidden="true" tabindex="-1"></a>                               #:subpattern (row (wildcard #:subject ls.car)))</span>
<span id="cb6-18"><a href="#cb6-18" aria-hidden="true" tabindex="-1"></a>                        (apply #:subject    ls</span>
<span id="cb6-19"><a href="#cb6-19" aria-hidden="true" tabindex="-1"></a>                               #:procedure  <span class="kw">cdr</span></span>
<span id="cb6-20"><a href="#cb6-20" aria-hidden="true" tabindex="-1"></a>                               #:vars       (ls.cdr)</span>
<span id="cb6-21"><a href="#cb6-21" aria-hidden="true" tabindex="-1"></a>                               #:subpattern (row (var #:subject ls.cdr</span>
<span id="cb6-22"><a href="#cb6-22" aria-hidden="true" tabindex="-1"></a>                                                      #:name ls*))))))</span>
<span id="cb6-23"><a href="#cb6-23" aria-hidden="true" tabindex="-1"></a>         (action do-last ls*)))</span></code></pre></div>
<p>We look at the first row, which only has one subpattern and thus only one potential specializer to pick: <code>(? #:subject ls #:predicate pair?)</code>. So we generate a <code>dt-test</code> node which checks if <code>ls</code> is a pair.</p>
<p>In the success branch of this <code>dt-test</code> node, we put the result of re-running this algorithm on all of the patacts with this specializer applied. We don’t need the <code>pair?</code> any more in either row, so it will disappear and be replaced by the right-hand side of the <code>and</code> pattern which contains it. This in turn is a row in both cases; we splice it into the main row of each patact. Our <em>specialized</em> patacts now look like this:</p>
<div class="sourceCode" id="cb7"><pre class="sourceCode scheme"><code class="sourceCode scheme"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a>((patact (row (apply #:subject    ls</span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a>                     #:procedure  <span class="kw">car</span></span>
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a>                     #:vars       (ls.car)</span>
<span id="cb7-4"><a href="#cb7-4" aria-hidden="true" tabindex="-1"></a>                     #:subpattern (row (var #:subject ls.car</span>
<span id="cb7-5"><a href="#cb7-5" aria-hidden="true" tabindex="-1"></a>                                            #:name x)))</span>
<span id="cb7-6"><a href="#cb7-6" aria-hidden="true" tabindex="-1"></a>              (apply #:subject    ls</span>
<span id="cb7-7"><a href="#cb7-7" aria-hidden="true" tabindex="-1"></a>                     #:procedure  <span class="kw">cdr</span></span>
<span id="cb7-8"><a href="#cb7-8" aria-hidden="true" tabindex="-1"></a>                     #:vars       (ls.cdr)</span>
<span id="cb7-9"><a href="#cb7-9" aria-hidden="true" tabindex="-1"></a>                     #:subpattern (row (<span class="kw">quote</span> #:subject ls.cdr</span>
<span id="cb7-10"><a href="#cb7-10" aria-hidden="true" tabindex="-1"></a>                                              #:datum ()))))</span>
<span id="cb7-11"><a href="#cb7-11" aria-hidden="true" tabindex="-1"></a>         (action return x))</span>
<span id="cb7-12"><a href="#cb7-12" aria-hidden="true" tabindex="-1"></a> (patact (row (apply #:subject    ls</span>
<span id="cb7-13"><a href="#cb7-13" aria-hidden="true" tabindex="-1"></a>                     #:procedure  <span class="kw">car</span></span>
<span id="cb7-14"><a href="#cb7-14" aria-hidden="true" tabindex="-1"></a>                     #:vars       (ls.car)</span>
<span id="cb7-15"><a href="#cb7-15" aria-hidden="true" tabindex="-1"></a>                     #:subpattern (row (wildcard #:subject ls.car)))</span>
<span id="cb7-16"><a href="#cb7-16" aria-hidden="true" tabindex="-1"></a>              (apply #:subject    ls</span>
<span id="cb7-17"><a href="#cb7-17" aria-hidden="true" tabindex="-1"></a>                     #:procedure  <span class="kw">cdr</span></span>
<span id="cb7-18"><a href="#cb7-18" aria-hidden="true" tabindex="-1"></a>                     #:vars       (ls.cdr)</span>
<span id="cb7-19"><a href="#cb7-19" aria-hidden="true" tabindex="-1"></a>                     #:subpattern (row (var #:subject ls.cdr</span>
<span id="cb7-20"><a href="#cb7-20" aria-hidden="true" tabindex="-1"></a>                                            #:name ls*))))</span>
<span id="cb7-21"><a href="#cb7-21" aria-hidden="true" tabindex="-1"></a>         (action do-last ls*)))</span></code></pre></div>
<p>As for the failure branch of <code>dt-test</code>: we know that the first patact can’t match any more just because this single test failed. We have to default on the second patact only – and that also required <code>(pair? ls)</code> to be true. So our <em>defaulted</em> patacts in this case are just the empty list, and the recursion on that will generate the leaf node to signal an exception because no clause matched that input.</p>
<p>Back on the success branch, we still have these two patacts, but this time the first row gives us a choice of what to do. We could pick the leftmost one – the car – but actually the car isn’t very interesting because it’s not refutable. The cdr, on the other hand, can fail, so let’s prioritize it. We generate a <code>dt-apply</code> binding the cdr of <code>ls</code> to <code>ls.cdr</code>; it only needs a success branch since we assume this can’t fail. So we specialize again, and the specialized rows, after splicing, look like this:</p>
<div class="sourceCode" id="cb8"><pre class="sourceCode scheme"><code class="sourceCode scheme"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a>((patact (row (apply #:subject    ls</span>
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a>                     #:procedure  <span class="kw">car</span></span>
<span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a>                     #:vars       (ls.car)</span>
<span id="cb8-4"><a href="#cb8-4" aria-hidden="true" tabindex="-1"></a>                     #:subpattern (row (var #:subject ls.car</span>
<span id="cb8-5"><a href="#cb8-5" aria-hidden="true" tabindex="-1"></a>                                            #:name x)))</span>
<span id="cb8-6"><a href="#cb8-6" aria-hidden="true" tabindex="-1"></a>              (<span class="kw">quote</span> #:subject ls.cdr</span>
<span id="cb8-7"><a href="#cb8-7" aria-hidden="true" tabindex="-1"></a>                     #:datum ()))</span>
<span id="cb8-8"><a href="#cb8-8" aria-hidden="true" tabindex="-1"></a>         (action return x))</span>
<span id="cb8-9"><a href="#cb8-9" aria-hidden="true" tabindex="-1"></a> (patact (row (apply #:subject    ls</span>
<span id="cb8-10"><a href="#cb8-10" aria-hidden="true" tabindex="-1"></a>                     #:procedure  <span class="kw">car</span></span>
<span id="cb8-11"><a href="#cb8-11" aria-hidden="true" tabindex="-1"></a>                     #:vars       (ls.car)</span>
<span id="cb8-12"><a href="#cb8-12" aria-hidden="true" tabindex="-1"></a>                     #:subpattern (row (wildcard #:subject ls.car)))</span>
<span id="cb8-13"><a href="#cb8-13" aria-hidden="true" tabindex="-1"></a>              (var #:subject ls.cdr</span>
<span id="cb8-14"><a href="#cb8-14" aria-hidden="true" tabindex="-1"></a>                   #:name ls*))</span>
<span id="cb8-15"><a href="#cb8-15" aria-hidden="true" tabindex="-1"></a>         (action do-last ls*)))</span></code></pre></div>
<p>Still two patterns in the top row, and again, the <code>quote</code> pattern looking at <code>ls.cdr</code> is still considerably more interesting than the <code>apply</code> pattern, so we pick that as the specializer again. We generate a <code>dt-equal</code> node checking if <code>ls.cdr</code> is the empty list. This one <em>does</em> need success and failure branches.</p>
<p>On the success branch, the specialized patterns look like this:</p>
<div class="sourceCode" id="cb9"><pre class="sourceCode scheme"><code class="sourceCode scheme"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a>((patact (row (apply #:subject    ls</span>
<span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a>                     #:procedure  <span class="kw">car</span></span>
<span id="cb9-3"><a href="#cb9-3" aria-hidden="true" tabindex="-1"></a>                     #:vars       (ls.car)</span>
<span id="cb9-4"><a href="#cb9-4" aria-hidden="true" tabindex="-1"></a>                     #:subpattern (row (var #:subject ls.car</span>
<span id="cb9-5"><a href="#cb9-5" aria-hidden="true" tabindex="-1"></a>                                            #:name x))))</span>
<span id="cb9-6"><a href="#cb9-6" aria-hidden="true" tabindex="-1"></a>         (action return x))</span>
<span id="cb9-7"><a href="#cb9-7" aria-hidden="true" tabindex="-1"></a> (patact (row (apply #:subject    ls</span>
<span id="cb9-8"><a href="#cb9-8" aria-hidden="true" tabindex="-1"></a>                     #:procedure  <span class="kw">car</span></span>
<span id="cb9-9"><a href="#cb9-9" aria-hidden="true" tabindex="-1"></a>                     #:vars       (ls.car)</span>
<span id="cb9-10"><a href="#cb9-10" aria-hidden="true" tabindex="-1"></a>                     #:subpattern (row (wildcard #:subject ls.car)))</span>
<span id="cb9-11"><a href="#cb9-11" aria-hidden="true" tabindex="-1"></a>              (var #:subject ls.cdr</span>
<span id="cb9-12"><a href="#cb9-12" aria-hidden="true" tabindex="-1"></a>                   #:name ls*))</span>
<span id="cb9-13"><a href="#cb9-13" aria-hidden="true" tabindex="-1"></a>         (action do-last ls*)))</span></code></pre></div>
<p>We have completely eliminated the cdr patterns from the top row! Only the irrefutable car pattern is left, and when the first row is irrefutable, the pattern has matched – so we generate the <code>dt-apply</code> node and its <code>dt-rename</code> child and have it point to the action which returns <code>x</code>. Done!</p>
<p>On the failure branch of the check for a null <code>ls.cdr</code>, we’ve thrown away the top row again and we end up with this single patact:</p>
<div class="sourceCode" id="cb10"><pre class="sourceCode scheme"><code class="sourceCode scheme"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a>((patact (row (apply #:subject    ls</span>
<span id="cb10-2"><a href="#cb10-2" aria-hidden="true" tabindex="-1"></a>                     #:procedure  <span class="kw">car</span></span>
<span id="cb10-3"><a href="#cb10-3" aria-hidden="true" tabindex="-1"></a>                     #:vars       (ls.car)</span>
<span id="cb10-4"><a href="#cb10-4" aria-hidden="true" tabindex="-1"></a>                     #:subpattern (row (wildcard #:subject ls.car)))</span>
<span id="cb10-5"><a href="#cb10-5" aria-hidden="true" tabindex="-1"></a>              (var #:subject ls.cdr</span>
<span id="cb10-6"><a href="#cb10-6" aria-hidden="true" tabindex="-1"></a>                   #:name ls*))</span>
<span id="cb10-7"><a href="#cb10-7" aria-hidden="true" tabindex="-1"></a>         (action do-last ls*)))</span></code></pre></div>
<p>Once again, this is irrefutable, so we again generate the <code>dt-apply</code> node for <code>ls.car</code>, the <code>dt-rename</code> node for the <code>ls*</code> variable, and then finish at the action.</p>
<h2 id="picking-a-pattern">Picking a pattern</h2>
<p>Okay so, at each step of the pattern matching process, we pick a specializer from the first row’s patterns, generate a node from it, and specialize/default the remainder of the patterns on it. But how do we pick which pattern to choose from the row?</p>
<p>We have to work out which pattern is going to help us find answers the fastest, probably by comparing against the patterns in different rows somehow. We need a <a href="https://en.wikipedia.org/wiki/Heuristic_(computer_science)">heuristic</a> – a scoring function to guess which pattern is most likely to help find the matching clause in the fewest nodes possible.</p>
<p>Maranget considers the question of which heuristics to use, assigning letters to each of a number of potential scoring functions and measuring the impact of many potential combinations of them. In Maranget’s terms, extensible-match uses heuristic fnr: the specializer picked has to be in the <em>f</em>irst row;<a href="#fn6" class="footnote-ref" id="fnref6" role="doc-noteref"><sup>6</sup></a> the specializer is the one which will specialize the most rows (most <em>n</em>eeded); if there’s a tie for the most needed specializer, we pick the pattern with the smallest combined size of the specialized and defaulted <em>r</em>ows. (If even that goes to a tie, just pick the leftmost tied specializer.) I didn’t benchmark this particularly: n seemed like an intuitively sensible heuristic; based the suggestion in Maranget’s paper, I later added r as an additional tie-breaker.</p>
<p>Some of the heuristics Maranget considers aren’t really applicable to Scheme. His ML version can generate nodes with more outward branches than just success and failure, because the ML compiler can generate a single switch over many types at once; Scheme detects different types by calling individual predicate procedures for each individual type, so that’s not possible, and neither is the heuristic b which picks the smallest switch to build, for example – so extensible-match also can’t use the combination of heuristics Maranget personally recommends, pba.</p>
<h2 id="so-why-was-it-so-hardwhy-didnt-you-do-it-slightly-differently">So why was it so hard/why didn’t you do it slightly differently</h2>
<p>As always, the hard thing was not the actual writing of the code, but working out what the right code to write was.</p>
<p>For example, you might ask why I wasn’t tempted to skip the step of representing decision trees as their own, reified type and simply recurse by generating new <code>match</code> expressions nested within the branches of an <code>if</code> or within the body of a <code>let</code> for the specializer we’ve chosen, for example.</p>
<p>This is, indeed, <em>very</em> tempting. If you’re implementing a compiler based on decision trees, you might even find <a href="https://link.springer.com/chapter/10.1007/10693148_7">some literature</a> suggesting that you could do exactly this. Surely, you’ll think, just like those Swap nodes in Maranget’s paper aren’t actually part of how it’s really compiled – surely you can avoid generating the other nodes per se too, and just go straight to generating the code which those nodes will eventually be turned into?</p>
<p><a href="https://codeberg.org/dpk/extensible-match/commit/3ef951528c823bf31ae1a3b99589df8a2094fa67">So you try it, and it doesn’t work, and you regret it, and you commit it to the repository anyway as a warning to your future self and to others.</a> Actually there were other things that were wrong with that first attempt – trying to insist that patterns form a matrix-like grid turned out to be the wrong thing for this kind of pattern input AST. My idea was that I really wanted to be able to property test the decision tree generator, ensuring that for any generated patterns and terms it always had the same result as the naïve style of generation shown above. I was thinking of decision tree generation as an optional optimization pass, rather than a fundamental transformation pass. This is the wrong mental model to have.</p>
<p>Anyway, the real reason that would have been the wrong approach even if it had worked is that it wouldn’t have allowed any optimization for code size or compilation speed. Wait whaaaaaat??</p>
<h2 id="optimizing-space-and-time">Optimizing space and time</h2>
<p>Yeah, remember how I mentioned about the potentially exponential code size if you choose decision trees, but only in cases where the tricks to avoid it start to fail? It’s time to talk about those tricks.</p>
<p>The most important trick is sharing of nodes. When two different branches in the tree end up doing the same things, they should actually re-converge and become the same branch again. This makes the decision tree actually a decision dag (directed acyclic graph).</p>
<p>To achieve this sharing of nodes between different code paths, extensible-match uses two tricks. The most important one is <a href="https://en.wikipedia.org/wiki/Hash_consing">hash consing</a>, a trick more generally applicable in functional programming. When you have immutable structures which might be created many times over with the same contents, you can maybe save memory by storing all instances of structures of that type in a hash table. When someone asks to create a new instance of the type, first check if there’s already one existing which has the contents they’re interested in. If so, just return that existing one instead of creating a new one. (But if not, be sure to put the newly-created object into the table before returning it, in case someone asks for another one of the same in future.)</p>
<p>Neither R6RS’s record system nor its hash tables were really meant to support doing this, and the <code>(extensible-match decision-tree)</code> library has to do some pretty undignified nonsense in order to make it work. But it does work, and saves both time and space: space obviously, but also time because it stresses the garbage collector less to have fewer objects sitting around.</p>
<p>There’s a second layer of caching which is similar, one I mentioned I was considering using in <a href="/posts/2025-09-29-extensible-match-decision-tree-story.html">an earlier post</a>: the recursive procedure which generates tree nodes from a (specialized or defaulted) set of rows and actions is memoized. This has a similar effect to hash consing but, since hash consing already guarantees sharing, it optimizes compilation time and not decision tree size. Memoization avoids repeating an exponential recursion entirely in many cases. I measured how many cache hits the memo table got for some complex patterns, and how much it sped things up, and it was totally worth it. The 14 cases over three list variables which used to take 9.3 seconds to compile on Guile now comes back almost immediately.</p>
<h2 id="visualization">Visualization</h2>
<p>What’s the most common target language for compilers? x86? The JVM? C? JavaScript? If I were a betting lass, I might be tempted to put money on the answer being <a href="https://graphviz.org/doc/info/lang.html">DOT</a>, the GraphViz file format. If you want to understand what your compiler is doing, the easiest way to see it is often to actually have it draw your control-flow graph, automaton, or decision dag in two dimensions. Outsourcing the actual drawing task to GraphViz is a nice sweet spot in the trade-off between getting it working and making it nice.<a href="#fn7" class="footnote-ref" id="fnref7" role="doc-noteref"><sup>7</sup></a></p>
<p>Initially I tried to see what was going on with a very silly (non-hygienic) code generator outputting Scheme code, but this quickly got tiring to read, especially because it couldn’t show sharing in the dag. (How the real code generator represents shared nodes is a topic for next time.) With a 2D graph, it’s relatively easy to get a rough impression on first glance of how well the decision tree generator handles certain cases; by following the arrows, you can fairly easily check for anything it’s doing wrong.</p>
<p>I’ve kept an archive of older DOT files showing how the pattern match compiler has got more capable over time, going back to <a href="/images/2024-12-02-bytecode-machine.png">some of the first decision trees this code ever generated,</a> before it was even hooked up to a code generator. (This DOT file is dated 2 December 2024, and I didn’t get the code generator working until Candlemas.) This lets me see how certain changes improved or disimproved the generated trees over time.</p>
<p>Originally, to review these DOT files, I had to manually copy the generated DOT source into a file and invoke the <code>dot</code> command line tool. But recently I thought, hey, I have a terminal emulator which can display inline graphics, so why not just show them directly in the REPL? So I wrote a little library to do just that.</p>
<details>
<summary>
Source code of the library which plops a drawing of a graph straight into my terminal
</summary>
<div class="sourceCode" id="cb11"><pre class="sourceCode scheme"><code class="sourceCode scheme"><span id="cb11-1"><a href="#cb11-1" aria-hidden="true" tabindex="-1"></a>(library (extensible-match dot)</span>
<span id="cb11-2"><a href="#cb11-2" aria-hidden="true" tabindex="-1"></a>  (export show-dot)</span>
<span id="cb11-3"><a href="#cb11-3" aria-hidden="true" tabindex="-1"></a>  (import (rnrs (<span class="dv">6</span>))</span>
<span id="cb11-4"><a href="#cb11-4" aria-hidden="true" tabindex="-1"></a>          (only (guile) mkstemp port-filename waitpid)</span>
<span id="cb11-5"><a href="#cb11-5" aria-hidden="true" tabindex="-1"></a>          (srfi :39)</span>
<span id="cb11-6"><a href="#cb11-6" aria-hidden="true" tabindex="-1"></a>          (ice-9 popen))</span>
<span id="cb11-7"><a href="#cb11-7" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb11-8"><a href="#cb11-8" aria-hidden="true" tabindex="-1"></a>  (<span class="ex">define-syntax</span><span class="fu"> show-dot</span></span>
<span id="cb11-9"><a href="#cb11-9" aria-hidden="true" tabindex="-1"></a>    (<span class="kw">syntax-rules</span> ()</span>
<span id="cb11-10"><a href="#cb11-10" aria-hidden="true" tabindex="-1"></a>      ((<span class="op">_</span> expr)</span>
<span id="cb11-11"><a href="#cb11-11" aria-hidden="true" tabindex="-1"></a>       (<span class="kw">let*</span> ((dot-port (mkstemp <span class="st">&quot;/tmp/dot-XXXXXX&quot;</span>))</span>
<span id="cb11-12"><a href="#cb11-12" aria-hidden="true" tabindex="-1"></a>              (dot-file (port-filename dot-port)))</span>
<span id="cb11-13"><a href="#cb11-13" aria-hidden="true" tabindex="-1"></a>         <span class="co">;; It was probably a mistake that decision-tree-&gt;dot writes</span></span>
<span id="cb11-14"><a href="#cb11-14" aria-hidden="true" tabindex="-1"></a>         <span class="co">;; to current-output-port, but I make the best of it:</span></span>
<span id="cb11-15"><a href="#cb11-15" aria-hidden="true" tabindex="-1"></a>         (parameterize ((<span class="kw">current-output-port</span> dot-port))</span>
<span id="cb11-16"><a href="#cb11-16" aria-hidden="true" tabindex="-1"></a>           expr)</span>
<span id="cb11-17"><a href="#cb11-17" aria-hidden="true" tabindex="-1"></a>         (<span class="kw">close-port</span> dot-port)</span>
<span id="cb11-18"><a href="#cb11-18" aria-hidden="true" tabindex="-1"></a>         (<span class="kw">let-values</span> (((from to pids)</span>
<span id="cb11-19"><a href="#cb11-19" aria-hidden="true" tabindex="-1"></a>                       (pipeline</span>
<span id="cb11-20"><a href="#cb11-20" aria-hidden="true" tabindex="-1"></a>                        `((<span class="st">&quot;dot&quot;</span> <span class="st">&quot;-Tpng&quot;</span> ,dot-file)</span>
<span id="cb11-21"><a href="#cb11-21" aria-hidden="true" tabindex="-1"></a>                          <span class="co">;; I tried out both sixel with img2sixel</span></span>
<span id="cb11-22"><a href="#cb11-22" aria-hidden="true" tabindex="-1"></a>                          <span class="co">;; (more widely supported) and the inline</span></span>
<span id="cb11-23"><a href="#cb11-23" aria-hidden="true" tabindex="-1"></a>                          <span class="co">;; bitmap functionality of iTerm2 (my</span></span>
<span id="cb11-24"><a href="#cb11-24" aria-hidden="true" tabindex="-1"></a>                          <span class="co">;; terminal emulator of choice) with this</span></span>
<span id="cb11-25"><a href="#cb11-25" aria-hidden="true" tabindex="-1"></a>                          <span class="co">;; imgcat program. Sixel was very fragile</span></span>
<span id="cb11-26"><a href="#cb11-26" aria-hidden="true" tabindex="-1"></a>                          <span class="co">;; when the terminal window got resized (the</span></span>
<span id="cb11-27"><a href="#cb11-27" aria-hidden="true" tabindex="-1"></a>                          <span class="co">;; bitmap area would just unrecoverably</span></span>
<span id="cb11-28"><a href="#cb11-28" aria-hidden="true" tabindex="-1"></a>                          <span class="co">;; blank out sometimes), so I went with</span></span>
<span id="cb11-29"><a href="#cb11-29" aria-hidden="true" tabindex="-1"></a>                          <span class="co">;; imgcat. It’s for my own use only, anyway,</span></span>
<span id="cb11-30"><a href="#cb11-30" aria-hidden="true" tabindex="-1"></a>                          <span class="co">;; and not too hard to change if needed:</span></span>
<span id="cb11-31"><a href="#cb11-31" aria-hidden="true" tabindex="-1"></a>                          (<span class="st">&quot;imgcat&quot;</span> <span class="st">&quot;--iterm2&quot;</span>)))))</span>
<span id="cb11-32"><a href="#cb11-32" aria-hidden="true" tabindex="-1"></a>           (<span class="kw">close-port</span> to)</span>
<span id="cb11-33"><a href="#cb11-33" aria-hidden="true" tabindex="-1"></a>           (<span class="kw">let</span> ((stdout (<span class="kw">standard-output-port</span>)))</span>
<span id="cb11-34"><a href="#cb11-34" aria-hidden="true" tabindex="-1"></a>             (<span class="kw">put-bytevector</span> stdout (<span class="kw">get-bytevector-all</span> from))</span>
<span id="cb11-35"><a href="#cb11-35" aria-hidden="true" tabindex="-1"></a>             (<span class="kw">flush-output-port</span> stdout))</span>
<span id="cb11-36"><a href="#cb11-36" aria-hidden="true" tabindex="-1"></a>           (<span class="kw">close-port</span> from)</span>
<span id="cb11-37"><a href="#cb11-37" aria-hidden="true" tabindex="-1"></a>           (<span class="kw">for-each</span> waitpid pids)))))))</span></code></pre></div>
</details>
<p>Enough of the talking, what do these decision trees actually look like, then? Here’s the drawing for the <code>last</code> example we worked through above.</p>
<svg width="352pt" viewBox="0 0 352 440" style="max-width:100%">
<g class="graph" transform="translate(4 436)"><path fill="#fff" d="M-4 4v-440h352V4z"/><g class="node"><path fill="none" stroke="#000" d="m236.5-432-62.49 18 62.49 18 62.49-18z"/><text x="236.5" y="-408.95" font-family="Times,serif" font-size="14" text-anchor="middle">(pair? ls)</text></g><g class="node"><path fill="none" stroke="#000" d="M253.75-343.5H89.25v36h164.5z"/><text x="171.5" y="-320.45" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (ls.cdr) (cdr ls) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M225.77-398.72c-9.38 12.48-23.23 30.91-34.59 46.03"/><path stroke="#000" d="m194.2-350.89-8.8 5.89 3.21-10.09z"/><text x="213.95" y="-364.7" font-family="Times,serif" font-size="14" text-anchor="middle">T</text></g><g class="node"><ellipse cx="301.5" cy="-325.5" fill="none" stroke="#000" rx="29.34" ry="18"/><text x="301.5" y="-320.45" font-family="Times,serif" font-size="14" text-anchor="middle">(fail)</text></g><g class="edge"><path fill="none" stroke="#000" d="M247.23-398.72c9.68 12.88 24.12 32.09 35.67 47.46"/><path stroke="#000" d="m285.53-353.58 3.21 10.1-8.81-5.89z"/><text x="278.57" y="-364.7" font-family="Times,serif" font-size="14" text-anchor="middle">F</text></g><g class="node"><path fill="none" stroke="#000" d="m171.5-270.5-105.19 18 105.19 18 105.19-18z"/><text x="171.5" y="-247.45" font-family="Times,serif" font-size="14" text-anchor="middle">(equal? ls.cdr '())</text></g><g class="edge"><path fill="none" stroke="#000" d="M171.5-307.31v25.27"/><path stroke="#000" d="m175-282.04-3.5 10-3.5-10z"/></g><g class="node"><path fill="none" stroke="#000" d="M163-182H0v36h163z"/><text x="81.5" y="-158.95" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (ls.car) (car ls) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="m156.23-236.83-48.3 46.43"/><path stroke="#000" d="m110.36-187.89-9.63 4.41 4.78-9.45z"/><text x="138.69" y="-203.2" font-family="Times,serif" font-size="14" text-anchor="middle">T</text></g><g class="node"><path fill="none" stroke="#000" d="M344-182H181v36h163z"/><text x="262.5" y="-158.95" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (ls.car) (car ls) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M186.94-236.83c13.47 12.81 33.31 31.67 49.28 46.85"/><path stroke="#000" d="m238.23-192.9 4.83 9.42-9.66-4.35z"/><text x="228.9" y="-203.2" font-family="Times,serif" font-size="14" text-anchor="middle">F</text></g><g class="node"><path fill="none" stroke="#000" d="M139-109H24v36h115z"/><text x="81.5" y="-85.95" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((x ls.car)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M81.5-145.81v25.27"/><path stroke="#000" d="m85-120.54-3.5 10-3.5-10z"/></g><g class="node"><path fill="none" stroke="#000" d="M324.88-109H200.12v36h124.76z"/><text x="262.5" y="-85.95" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((ls* ls.cdr)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M262.5-145.81v25.27"/><path stroke="#000" d="m266-120.54-3.5 10-3.5-10z"/></g><g class="node"><ellipse cx="81.5" cy="-18" fill="none" stroke="#000" rx="46.23" ry="18"/><text x="81.5" y="-12.95" font-family="Times,serif" font-size="14" text-anchor="middle">(return x)</text></g><g class="edge"><path fill="none" stroke="#000" d="M81.5-72.81v25.27"/><path stroke="#000" d="m85-47.54-3.5 10-3.5-10z"/></g><g class="node"><ellipse cx="262.5" cy="-18" fill="none" stroke="#000" rx="55.45" ry="18"/><text x="262.5" y="-12.95" font-family="Times,serif" font-size="14" text-anchor="middle">(do-last ls*)</text></g><g class="edge"><path fill="none" stroke="#000" d="M262.5-72.81v25.27"/><path stroke="#000" d="m266-47.54-3.5 10-3.5-10z"/></g></g>
</svg>
<p>The shapes of the nodes are the classic flowchart style: diamonds for decisions, rectangles for steps, and circles for termination.</p>
<p>Note that the <code>do-last</code> path also generates a call to <code>(car ls)</code> even though it doesn’t use the value of the car. In theory, it should be smart enough not to do this, but I decided for now to leave that optimization to the Scheme compiler on the theory that it can probably do a better job of it anyway.</p>
<p>For another simple example, this is a type-safe list merge.</p>
<div class="sourceCode" id="cb12"><pre class="sourceCode scheme"><code class="sourceCode scheme"><span id="cb12-1"><a href="#cb12-1" aria-hidden="true" tabindex="-1"></a>(<span class="ex">define</span><span class="fu"> list-merge</span></span>
<span id="cb12-2"><a href="#cb12-2" aria-hidden="true" tabindex="-1"></a>  (match-lambda</span>
<span id="cb12-3"><a href="#cb12-3" aria-hidden="true" tabindex="-1"></a>    ((&#39;() &#39;())  &#39;())</span>
<span id="cb12-4"><a href="#cb12-4" aria-hidden="true" tabindex="-1"></a>    (((<span class="op">?</span> <span class="kw">pair?</span> xs) &#39;())  xs)</span>
<span id="cb12-5"><a href="#cb12-5" aria-hidden="true" tabindex="-1"></a>    ((&#39;() (<span class="op">?</span> <span class="kw">pair?</span> ys))  ys)</span>
<span id="cb12-6"><a href="#cb12-6" aria-hidden="true" tabindex="-1"></a>    (((<span class="kw">cons</span> x xs*) (<span class="kw">cons</span> y ys*))</span>
<span id="cb12-7"><a href="#cb12-7" aria-hidden="true" tabindex="-1"></a>     (<span class="kw">if</span> (<span class="op">&lt;</span> y x)</span>
<span id="cb12-8"><a href="#cb12-8" aria-hidden="true" tabindex="-1"></a>         (<span class="kw">cons</span> y (list-merge (<span class="kw">cons</span> x xs*) ys*))</span>
<span id="cb12-9"><a href="#cb12-9" aria-hidden="true" tabindex="-1"></a>         (<span class="kw">cons</span> x (list-merge xs* (<span class="kw">cons</span> y ys*)))))))</span></code></pre></div>
<svg width="712pt" viewBox="0 0 712 983" style="max-width:100%;">
<g class="graph" transform="translate(4 979.2)"><path fill="#fff" d="M-4 4v-983.2h712.09V4z"/><g class="node"><path fill="none" stroke="#000" d="m252.76-975.2-102.99 18 102.99 18 103-18z"/><text x="252.76" y="-953" font-family="Times,serif" font-size="14" text-anchor="middle">(equal? arg1 '())</text></g><g class="node"><path fill="none" stroke="#000" d="m152.76-886.4-102.99 18 102.99 18 103-18z"/><text x="152.76" y="-864.2" font-family="Times,serif" font-size="14" text-anchor="middle">(equal? arg2 '())</text></g><g class="edge"><path fill="none" stroke="#000" d="M236.25-941.87c-15.85 13.76-40.01 34.73-58.27 50.58"/><path stroke="#000" d="m180.29-888.66-9.84 3.91 5.25-9.19z"/><text x="216" y="-908.6" font-family="Times,serif" font-size="14" text-anchor="middle">T</text></g><g class="node"><path fill="none" stroke="#000" d="m353.76-886.4-79.67 18 79.67 18 79.68-18z"/><text x="353.76" y="-864.2" font-family="Times,serif" font-size="14" text-anchor="middle">(pair? arg1)</text></g><g class="edge"><path fill="none" stroke="#000" d="M269.44-941.87c16.28 13.99 41.23 35.43 59.77 51.37"/><path stroke="#000" d="m331.28-893.34 5.3 9.17-9.86-3.86z"/><text x="316.2" y="-908.6" font-family="Times,serif" font-size="14" text-anchor="middle">F</text></g><g class="node"><ellipse cx="58.76" cy="-779.6" fill="none" stroke="#000" rx="58.76" ry="18"/><text x="58.76" y="-775.4" font-family="Times,serif" font-size="14" text-anchor="middle">(return-null)</text></g><g class="edge"><path fill="none" stroke="#000" d="M137.24-853.07c-14.21 13.12-35.52 32.81-52.35 48.35"/><path stroke="#000" d="m87.39-802.27-9.72 4.21 4.97-9.35z"/><text x="118.46" y="-819.8" font-family="Times,serif" font-size="14" text-anchor="middle">T</text></g><g class="node"><path fill="none" stroke="#000" d="m214.76-797.6-79.67 18 79.67 18 79.68-18z"/><text x="214.76" y="-775.4" font-family="Times,serif" font-size="14" text-anchor="middle">(pair? arg2)</text></g><g class="edge"><path fill="none" stroke="#000" d="M163.85-851.88c9.37 13.12 22.95 32.13 33.73 47.23"/><path stroke="#000" d="m200.35-806.81 2.96 10.17-8.66-6.1z"/><text x="193.21" y="-819.8" font-family="Times,serif" font-size="14" text-anchor="middle">F</text></g><g class="node"><ellipse cx="311.76" cy="-602" fill="none" stroke="#000" rx="30.34" ry="18"/><text x="311.76" y="-597.8" font-family="Times,serif" font-size="14" text-anchor="middle">(fail)</text></g><g class="edge"><path fill="none" stroke="#000" d="M351.14-850.86c-7 44.03-26.13 164.47-34.89 219.61"/><path stroke="#000" d="m319.76-631.04-5.02 9.33-1.89-10.43z"/><text x="337.63" y="-731" font-family="Times,serif" font-size="14" text-anchor="middle">F</text></g><g class="node"><path fill="none" stroke="#000" d="m460.76-797.6-102.99 18 102.99 18 103-18z"/><text x="460.76" y="-775.4" font-family="Times,serif" font-size="14" text-anchor="middle">(equal? arg2 '())</text></g><g class="edge"><path fill="none" stroke="#000" d="M370.48-853.84c17.05 13.83 43.76 35.5 63.71 51.68"/><path stroke="#000" d="m436.36-804.9 5.56 9.01-9.97-3.58z"/><text x="421.12" y="-819.8" font-family="Times,serif" font-size="14" text-anchor="middle">T</text></g><g class="node"><path fill="none" stroke="#000" d="M257.91-708.8H141.62v36h116.29z"/><text x="199.76" y="-686.6" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((ys arg2)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M211.87-761.86c-2.03 11.73-4.79 27.71-7.16 41.45"/><path stroke="#000" d="m208.16-719.83-5.15 9.26-1.75-10.45z"/><text x="212.88" y="-731" font-family="Times,serif" font-size="14" text-anchor="middle">T</text></g><g class="edge"><path fill="none" stroke="#000" d="M227.21-764.06c11.29 13.64 27.89 34.95 39.55 55.26 14.56 25.35 27.19 56.33 35.34 78.19"/><path stroke="#000" d="m305.28-632.1.13 10.6-6.71-8.2z"/><text x="288.06" y="-686.6" font-family="Times,serif" font-size="14" text-anchor="middle">F</text></g><g class="node"><ellipse cx="199.76" cy="-602" fill="none" stroke="#000" rx="63.33" ry="18"/><text x="199.76" y="-597.8" font-family="Times,serif" font-size="14" text-anchor="middle">(return-ys ys)</text></g><g class="edge"><path fill="none" stroke="#000" d="M199.76-672.65v41.03"/><path stroke="#000" d="m203.26-631.92-3.5 10-3.5-10z"/></g><g class="node"><path fill="none" stroke="#000" d="M686.91-708.8H570.62v36h116.29z"/><text x="628.76" y="-686.6" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((xs arg1)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="m485.92-765.61 99.23 51.28"/><path stroke="#000" d="m586.39-717.63 7.28 7.7-10.49-1.48z"/><text x="564.09" y="-731" font-family="Times,serif" font-size="14" text-anchor="middle">T</text></g><g class="node"><path fill="none" stroke="#000" d="m459.76-708.8-79.67 18 79.67 18 79.68-18z"/><text x="459.76" y="-686.6" font-family="Times,serif" font-size="14" text-anchor="middle">(pair? arg2)</text></g><g class="edge"><path fill="none" stroke="#000" d="M460.57-761.45c-.14 11.69-.32 27.46-.48 41.03"/><path stroke="#000" d="m463.6-720.68-3.62 9.96-3.38-10.04z"/><text x="464.25" y="-731" font-family="Times,serif" font-size="14" text-anchor="middle">F</text></g><g class="node"><ellipse cx="640.76" cy="-602" fill="none" stroke="#000" rx="63.33" ry="18"/><text x="640.76" y="-597.8" font-family="Times,serif" font-size="14" text-anchor="middle">(return-xs xs)</text></g><g class="edge"><path fill="none" stroke="#000" d="M631.14-672.65c1.61 11.69 3.79 27.46 5.67 41.03"/><path stroke="#000" d="m640.25-632.29-2.1 10.38-4.84-9.43z"/></g><g class="edge"><path fill="none" stroke="#000" d="M438.24-677.18c-25.13 14.74-66.9 39.24-95.52 56.02"/><path stroke="#000" d="m344.53-618.16-10.39 2.04 6.85-8.08z"/><text x="402.91" y="-642.2" font-family="Times,serif" font-size="14" text-anchor="middle">F</text></g><g class="node"><path fill="none" stroke="#000" d="M559.67-620H359.86v36h199.81z"/><text x="459.76" y="-597.8" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (arg1_car) (car arg1) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M459.76-672.65v41.03"/><path stroke="#000" d="m463.26-631.92-3.5 10-3.5-10z"/><text x="464.04" y="-642.2" font-family="Times,serif" font-size="14" text-anchor="middle">T</text></g><g class="node"><path fill="none" stroke="#000" d="M527.23-547H392.3v36h134.93z"/><text x="459.76" y="-524.8" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((x arg1_car)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M459.76-583.81v25.27"/><path stroke="#000" d="m463.26-558.54-3.5 10-3.5-10z"/></g><g class="node"><path fill="none" stroke="#000" d="M560.46-474H359.07v36h201.39z"/><text x="459.76" y="-451.8" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (arg1_cdr) (cdr arg1) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M459.76-510.81v25.27"/><path stroke="#000" d="m463.26-485.54-3.5 10-3.5-10z"/></g><g class="node"><path fill="none" stroke="#000" d="M533.85-401H385.68v36h148.17z"/><text x="459.76" y="-378.8" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((xs* arg1_cdr)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M459.76-437.81v25.27"/><path stroke="#000" d="m463.26-412.54-3.5 10-3.5-10z"/></g><g class="node"><path fill="none" stroke="#000" d="M559.67-328H359.86v36h199.81z"/><text x="459.76" y="-305.8" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (arg2_car) (car arg2) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M459.76-364.81v25.27"/><path stroke="#000" d="m463.26-339.54-3.5 10-3.5-10z"/></g><g class="node"><path fill="none" stroke="#000" d="M527.23-255H392.3v36h134.93z"/><text x="459.76" y="-232.8" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((y arg2_car)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M459.76-291.81v25.27"/><path stroke="#000" d="m463.26-266.54-3.5 10-3.5-10z"/></g><g class="node"><path fill="none" stroke="#000" d="M560.46-182H359.07v36h201.39z"/><text x="459.76" y="-159.8" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (arg2_cdr) (cdr arg2) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M459.76-218.81v25.27"/><path stroke="#000" d="m463.26-193.54-3.5 10-3.5-10z"/></g><g class="node"><path fill="none" stroke="#000" d="M533.85-109H385.68v36h148.17z"/><text x="459.76" y="-86.8" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((ys* arg2_cdr)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M459.76-145.81v25.27"/><path stroke="#000" d="m463.26-120.54-3.5 10-3.5-10z"/></g><g class="node"><ellipse cx="459.76" cy="-18" fill="none" stroke="#000" rx="100.61" ry="18"/><text x="459.76" y="-13.8" font-family="Times,serif" font-size="14" text-anchor="middle">(do-merge x xs* y ys*)</text></g><g class="edge"><path fill="none" stroke="#000" d="M459.76-72.81v25.27"/><path stroke="#000" d="m463.26-47.54-3.5 10-3.5-10z"/></g></g>
</svg>
<p>You can see the distinction between subject variables (<code>arg1</code>, <code>arg2</code>, <code>arg1_car</code>, <code>arg1_cdr</code>, <code>arg2_car</code>, <code>arg2_cdr</code>) and pattern variables. You can follow through and see how it avoids repeating the <code>pair?</code> check on both arguments for the final clause – there are two different <code>pair?</code> checks on <code>arg2</code>, but they’re in mutually exclusive code paths. There’s a long chain of <code>dt-apply</code> (<code>receive</code>) and <code>dt-rename</code> (<code>let</code>) nodes at the end, because the decision tree generator puts off those steps, which don’t affect whether the patterns match or not, until the end.</p>
<p>For a more complex example, let’s look at Chris Okasaki’s red-black tree balancing pattern (<a href="https://doi.org/10.1017/S0956796899003494">paper</a>, <a href="https://doi.org/10.1017/CBO9780511530104">book</a>). This is probably pattern match compiler authors’ favourite example to use, because</p>
<ol type="1">
<li><p>it’s nontrivial</p></li>
<li><p>it’s real and not contrived</p></li>
<li><p>it’s a ‘hot’ function which gets called <span class="math inline">log<sub>2</sub><em>n</em></span> times for every insertion into a tree of size <span class="math inline"><em>n</em></span>, making it important to optimize well</p></li>
<li><p>it’s a pretty amazing example of using pattern matching to translate reasoning about data structure invariants directly into running code, in a way that would be difficult without pattern matching</p></li>
</ol>
<p>What does it do? Basically a red-black tree stays relatively well balanced by colouring the levels of nodes red or black, and maintaining two properties about where coloured nodes are allowed to appear relative to one another. One of these rules is that no red node is allowed to touch another red node: black touching black is okay; red touching red isn’t. So Okasaki works out that after finding a place to insert a new red node, there are exactly four ways in which a tree can end up ‘unbalanced’ in having two adjacent red nodes; in that case the tree needs to be ‘rotated’ a little to restore balance.</p>
<p>He writes that like this (but in ML):</p>
<div class="sourceCode" id="cb13"><pre class="sourceCode scheme"><code class="sourceCode scheme"><span id="cb13-1"><a href="#cb13-1" aria-hidden="true" tabindex="-1"></a>(<span class="ex">define</span><span class="fu"> </span>(balance n)</span>
<span id="cb13-2"><a href="#cb13-2" aria-hidden="true" tabindex="-1"></a>  (match n</span>
<span id="cb13-3"><a href="#cb13-3" aria-hidden="true" tabindex="-1"></a>    ((<span class="kw">or</span> (node &#39;black (node &#39;red (node &#39;red a x b) y c) z d)</span>
<span id="cb13-4"><a href="#cb13-4" aria-hidden="true" tabindex="-1"></a>         (node &#39;black (node &#39;red a x (node &#39;red b y c)) z d)</span>
<span id="cb13-5"><a href="#cb13-5" aria-hidden="true" tabindex="-1"></a>         (node &#39;black a x (node &#39;red (node &#39;red b y c) z d))</span>
<span id="cb13-6"><a href="#cb13-6" aria-hidden="true" tabindex="-1"></a>         (node &#39;black a x (node &#39;red b y (node &#39;red c z d))))</span>
<span id="cb13-7"><a href="#cb13-7" aria-hidden="true" tabindex="-1"></a>     (node &#39;red (node &#39;black a x b) y (node &#39;black c z d)))</span>
<span id="cb13-8"><a href="#cb13-8" aria-hidden="true" tabindex="-1"></a>    (<span class="op">_</span> n)))</span></code></pre></div>
<p><code>x</code>, <code>y</code>, and <code>z</code> are values in the tree; <code>a</code>, <code>b</code>, <code>c</code>, and <code>d</code> are child nodes. The sort order of the values and the values in the child nodes is <code>a</code> &lt; <code>x</code> &lt; <code>b</code> &lt; <code>y</code> &lt; <code>c</code> &lt; <code>z</code> &lt; <code>d</code>. Visually explained, the job of this procedure is to turn one of these wrong trees:</p>
<!--
digraph {
ordering=out
subgraph LL {
    LLa[label=a, shape=none]
    LLx[label=x, style=filled, fillcolor=red, fontcolor=white, shape=circle]
    LLb[label=b, shape=none]
    LLy[label=y, style=filled, fillcolor=red, fontcolor=white, shape=circle]
    LLc[label=c, shape=none]
    LLz[label=z, style=filled, fillcolor=black, fontcolor=white, shape=circle]
    LLd[label=d, shape=none]
    LLz -> LLy
    LLy -> LLx
    LLz -> LLd
    LLy -> LLc
    LLx -> LLa
    LLx -> LLb
}

subgraph LR {
    LRa[label=a, shape=none]
    LRx[label=x, style=filled, fillcolor=red, fontcolor=white, shape=circle]
    LRb[label=b, shape=none]
    LRy[label=y, style=filled, fillcolor=red, fontcolor=white, shape=circle]
    LRc[label=c, shape=none]
    LRz[label=z, style=filled, fillcolor=black, fontcolor=white, shape=circle]
    LRd[label=d, shape=none]
    LRz -> LRx
    LRz -> LRd
    LRx -> LRa
    LRx -> LRy
    LRy -> LRb
    LRy -> LRc
}

subgraph RL {
    RLa[label=a, shape=none]
    RLx[label=x, style=filled, fillcolor=black, fontcolor=white, shape=circle]
    RLb[label=b, shape=none]
    RLy[label=y, style=filled, fillcolor=red, fontcolor=white, shape=circle]
    RLc[label=c, shape=none]
    RLz[label=z, style=filled, fillcolor=red, fontcolor=white, shape=circle]
    RLd[label=d, shape=none]
    RLx -> RLa
    RLx -> RLz
    RLz -> RLy
    RLz -> RLd
    RLy -> RLb
    RLy -> RLc
}

subgraph RR {
    RRa[label=a, shape=none]
    RRx[label=x, style=filled, fillcolor=black, fontcolor=white, shape=circle]
    RRb[label=b, shape=none]
    RRy[label=y, style=filled, fillcolor=red, fontcolor=white, shape=circle]
    RRc[label=c, shape=none]
    RRz[label=z, style=filled, fillcolor=red, fontcolor=white, shape=circle]
    RRd[label=d, shape=none]
    RRx -> RRa
    RRx -> RRy
    RRy -> RRb
    RRy -> RRz
    RRz -> RRc
    RRz -> RRd
}

}
-->
<svg width="836" viewBox="0 0 627 260" style="max-width: 100%;">
<g class="graph"><path fill="#fff" d="M0 260V0h627v260z"/><g class="node" transform="translate(4 256)"><text x="27" y="-13.8" font-size="14" text-anchor="middle">a</text></g><g class="node" transform="translate(4 256)"><circle cx="31" cy="-90" r="18" fill="red" stroke="#000"/><text x="31" y="-85.8" fill="#fff" font-size="14" text-anchor="middle">x</text></g><g class="edge" transform="translate(4 256)"><path fill="none" stroke="#000" d="M30.01-71.7c-.43 7.49-.94 16.44-1.42 24.82"/><path stroke="#000" d="m32.1-46.92-4.06 9.79-2.93-10.19z"/></g><g class="node" transform="translate(4 256)"><text x="99" y="-13.8" font-size="14" text-anchor="middle">b</text></g><g class="edge" transform="translate(4 256)"><path fill="none" stroke="#000" d="M43.16-76.49c8.76 9.03 20.89 21.51 31.61 32.54"/><path stroke="#000" d="m77.09-46.58 4.46 9.62-9.48-4.74z"/></g><g class="node" transform="translate(4 256)"><circle cx="54" cy="-162" r="18" fill="red" stroke="#000"/><text x="54" y="-157.8" fill="#fff" font-size="14" text-anchor="middle">y</text></g><g class="edge" transform="translate(4 256)"><path fill="none" stroke="#000" d="M48.55-144.41c-2.58 7.84-5.72 17.42-8.64 26.28"/><path stroke="#000" d="m43.33-117.32-6.45 8.41-.2-10.59z"/></g><g class="node" transform="translate(4 256)"><text x="94" y="-85.8" font-size="14" text-anchor="middle">c</text></g><g class="edge" transform="translate(4 256)"><path fill="none" stroke="#000" d="M62.68-145.81c4.72 8.27 10.71 18.73 16.17 28.3"/><path stroke="#000" d="m81.82-119.36 1.93 10.42-8-6.95z"/></g><g class="node" transform="translate(4 256)"><circle cx="86" cy="-234" r="18" stroke="#000"/><text x="86" y="-229.8" fill="#fff" font-size="14" text-anchor="middle">z</text></g><g class="edge" transform="translate(4 256)"><path fill="none" stroke="#000" d="M78.74-217.12c-3.77 8.24-8.46 18.51-12.75 27.88"/><path stroke="#000" d="m69.2-187.84-7.34 7.64.98-10.55z"/></g><g class="node" transform="translate(4 256)"><text x="117" y="-157.8" font-size="14" text-anchor="middle">d</text></g><g class="edge" transform="translate(4 256)"><path fill="none" stroke="#000" d="M93.03-217.12c3.55 8.01 7.94 17.93 12 27.09"/><path stroke="#000" d="m108.1-191.74.85 10.56-7.25-7.73z"/></g><g class="node" transform="translate(4 256)"><text x="191" y="-85.8" font-size="14" text-anchor="middle">a</text></g><g class="node" transform="translate(4 256)"><circle cx="206" cy="-162" r="18" fill="red" stroke="#000"/><text x="206" y="-157.8" fill="#fff" font-size="14" text-anchor="middle">x</text></g><g class="edge" transform="translate(4 256)"><path fill="none" stroke="#000" d="M202.37-144.05c-1.62 7.56-3.58 16.68-5.4 25.2"/><path stroke="#000" d="m200.43-118.31-5.52 9.04-1.32-10.51z"/></g><g class="node" transform="translate(4 256)"><circle cx="254" cy="-90" r="18" fill="red" stroke="#000"/><text x="254" y="-85.8" fill="#fff" font-size="14" text-anchor="middle">y</text></g><g class="edge" transform="translate(4 256)"><path fill="none" stroke="#000" d="M215.95-146.5c6.24 9.11 14.44 21.07 21.63 31.55"/><path stroke="#000" d="m240.44-116.96 2.77 10.23-8.54-6.27z"/></g><g class="node" transform="translate(4 256)"><text x="191" y="-13.8" font-size="14" text-anchor="middle">b</text></g><g class="edge" transform="translate(4 256)"><path fill="none" stroke="#000" d="M242.15-75.83c-7.96 8.85-18.72 20.79-28.31 31.45"/><path stroke="#000" d="m216.45-42.04-9.3 5.09 4.09-9.77z"/></g><g class="node" transform="translate(4 256)"><text x="263" y="-13.8" font-size="14" text-anchor="middle">c</text></g><g class="edge" transform="translate(4 256)"><path fill="none" stroke="#000" d="M256.22-71.7c.97 7.49 2.12 16.44 3.2 24.82"/><path stroke="#000" d="m262.87-47.49-2.2 10.37-4.75-9.48z"/></g><g class="node" transform="translate(4 256)"><circle cx="238" cy="-234" r="18" stroke="#000"/><text x="238" y="-229.8" fill="#fff" font-size="14" text-anchor="middle">z</text></g><g class="edge" transform="translate(4 256)"><path fill="none" stroke="#000" d="M230.74-217.12c-3.77 8.24-8.46 18.51-12.75 27.88"/><path stroke="#000" d="m221.2-187.84-7.34 7.64.98-10.55z"/></g><g class="node" transform="translate(4 256)"><text x="269" y="-157.8" font-size="14" text-anchor="middle">d</text></g><g class="edge" transform="translate(4 256)"><path fill="none" stroke="#000" d="M245.03-217.12c3.55 8.01 7.94 17.93 12 27.09"/><path stroke="#000" d="m260.1-191.74.85 10.56-7.25-7.73z"/></g><g class="node" transform="translate(4 256)"><text x="350" y="-157.8" font-size="14" text-anchor="middle">a</text></g><g class="node" transform="translate(4 256)"><circle cx="382" cy="-234" r="18" stroke="#000"/><text x="382" y="-229.8" fill="#fff" font-size="14" text-anchor="middle">x</text></g><g class="edge" transform="translate(4 256)"><path fill="none" stroke="#000" d="M374.74-217.12c-3.66 8.01-8.2 17.93-12.38 27.09"/><path stroke="#000" d="m365.65-188.81-7.34 7.64.97-10.55z"/></g><g class="node" transform="translate(4 256)"><circle cx="413" cy="-162" r="18" fill="red" stroke="#000"/><text x="413" y="-157.8" fill="#fff" font-size="14" text-anchor="middle">z</text></g><g class="edge" transform="translate(4 256)"><path fill="none" stroke="#000" d="m389.03-217.12 12.35 27.88"/><path stroke="#000" d="m404.53-190.77.85 10.56-7.25-7.72z"/></g><g class="node" transform="translate(4 256)"><text x="345" y="-13.8" font-size="14" text-anchor="middle">b</text></g><g class="node" transform="translate(4 256)"><circle cx="365" cy="-90" r="18" fill="red" stroke="#000"/><text x="365" y="-85.8" fill="#fff" font-size="14" text-anchor="middle">y</text></g><g class="edge" transform="translate(4 256)"><path fill="none" stroke="#000" d="M360.26-72.41c-2.18 7.65-4.84 16.93-7.31 25.6"/><path stroke="#000" d="m356.37-46.07-6.11 8.66-.62-10.58z"/></g><g class="node" transform="translate(4 256)"><text x="417" y="-13.8" font-size="14" text-anchor="middle">c</text></g><g class="edge" transform="translate(4 256)"><path fill="none" stroke="#000" d="M375.52-74.83c6.35 8.53 14.62 19.67 22.11 29.75"/><path stroke="#000" d="m400.42-47.19 3.15 10.12-8.77-5.94z"/></g><g class="edge" transform="translate(4 256)"><path fill="none" stroke="#000" d="M403.05-146.5c-6.24 9.11-14.44 21.07-21.63 31.55"/><path stroke="#000" d="m384.33-113-8.54 6.27 2.77-10.23z"/></g><g class="node" transform="translate(4 256)"><text x="428" y="-85.8" font-size="14" text-anchor="middle">d</text></g><g class="edge" transform="translate(4 256)"><path fill="none" stroke="#000" d="M416.63-144.05c1.62 7.56 3.58 16.68 5.4 25.2"/><path stroke="#000" d="m425.41-119.78-1.32 10.51-5.52-9.04z"/></g><g class="node" transform="translate(4 256)"><text x="485" y="-157.8" font-size="14" text-anchor="middle">a</text></g><g class="node" transform="translate(4 256)"><circle cx="516" cy="-234" r="18" stroke="#000"/><text x="516" y="-229.8" fill="#fff" font-size="14" text-anchor="middle">x</text></g><g class="edge" transform="translate(4 256)"><path fill="none" stroke="#000" d="M508.97-217.12c-3.55 8.01-7.94 17.93-12 27.09"/><path stroke="#000" d="m500.3-188.91-7.25 7.73.85-10.56z"/></g><g class="node" transform="translate(4 256)"><circle cx="548" cy="-162" r="18" fill="red" stroke="#000"/><text x="548" y="-157.8" fill="#fff" font-size="14" text-anchor="middle">y</text></g><g class="edge" transform="translate(4 256)"><path fill="none" stroke="#000" d="M523.26-217.12c3.77 8.24 8.46 18.51 12.75 27.88"/><path stroke="#000" d="m539.16-190.75.98 10.55-7.34-7.64z"/></g><g class="node" transform="translate(4 256)"><text x="509" y="-85.8" font-size="14" text-anchor="middle">b</text></g><g class="edge" transform="translate(4 256)"><path fill="none" stroke="#000" d="M539.54-145.81c-4.61 8.27-10.44 18.73-15.77 28.3"/><path stroke="#000" d="m526.93-115.98-7.93 7.03 1.81-10.44z"/></g><g class="node" transform="translate(4 256)"><circle cx="572" cy="-90" r="18" fill="red" stroke="#000"/><text x="572" y="-85.8" fill="#fff" font-size="14" text-anchor="middle">z</text></g><g class="edge" transform="translate(4 256)"><path fill="none" stroke="#000" d="M553.69-144.41c2.69 7.84 5.97 17.42 9.01 26.28"/><path stroke="#000" d="m565.93-119.5-.07 10.6-6.55-8.33z"/></g><g class="node" transform="translate(4 256)"><text x="520" y="-13.8" font-size="14" text-anchor="middle">c</text></g><g class="edge" transform="translate(4 256)"><path fill="none" stroke="#000" d="M561.48-74.83c-6.35 8.53-14.62 19.67-22.11 29.75"/><path stroke="#000" d="m542.2-43.01-8.77 5.94 3.15-10.12z"/></g><g class="node" transform="translate(4 256)"><text x="592" y="-13.8" font-size="14" text-anchor="middle">d</text></g><g class="edge" transform="translate(4 256)"><path fill="none" stroke="#000" d="M576.74-72.41c2.18 7.65 4.84 16.93 7.31 25.6"/><path stroke="#000" d="m587.36-47.99-.62 10.58-6.11-8.66z"/></g></g>
</svg>
<p>Into this right tree:</p>
<!--
digraph {
ordering=out
    a[label=a, shape=none]
    x[label=x, style=filled, fillcolor=black, fontcolor=white, shape=circle]
    b[label=b, shape=none]
    y[label=y, style=filled, fillcolor=red, fontcolor=white, shape=circle]
    c[label=c, shape=none]
    z[label=z, style=filled, fillcolor=black, fontcolor=white, shape=circle]
    d[label=d, shape=none]
    y -> x
    y -> z
    x -> a
    x -> b
    z -> c
    z -> d
}
-->
<svg width="278pt" viewBox="0 0 278 188" style="max-width: 100%;">
<g class="graph"><path fill="#fff" d="M0 188V0h278v188z"/><g class="node" transform="translate(4 184)"><text x="27" y="-13.8" font-size="14" text-anchor="middle">a</text></g><g class="node" transform="translate(4 184)"><circle cx="99" cy="-90" r="18" stroke="#000"/><text x="99" y="-85.8" fill="#fff" font-size="14" text-anchor="middle">x</text></g><g class="edge" transform="translate(4 184)"><path fill="none" stroke="#000" d="M86.46-76.81c-9.4 9.14-22.59 21.96-34.15 33.2"/><path stroke="#000" d="m54.95-41.29-9.61 4.46 4.73-9.48z"/></g><g class="node" transform="translate(4 184)"><text x="99" y="-13.8" font-size="14" text-anchor="middle">b</text></g><g class="edge" transform="translate(4 184)"><path fill="none" stroke="#000" d="M99-71.7v24.82"/><path stroke="#000" d="m102.5-47.14-3.5 10-3.5-10z"/></g><g class="node" transform="translate(4 184)"><circle cx="135" cy="-162" r="18" fill="red" stroke="#000"/><text x="135" y="-157.8" fill="#fff" font-size="14" text-anchor="middle">y</text></g><g class="edge" transform="translate(4 184)"><path fill="none" stroke="#000" d="M127.01-145.46c-4.38 8.51-9.92 19.29-14.92 29"/><path stroke="#000" d="m115.35-115.13-7.69 7.29 1.46-10.49z"/></g><g class="node" transform="translate(4 184)"><circle cx="171" cy="-90" r="18" stroke="#000"/><text x="171" y="-85.8" fill="#fff" font-size="14" text-anchor="middle">z</text></g><g class="edge" transform="translate(4 184)"><path fill="none" stroke="#000" d="M142.99-145.46c4.38 8.51 9.92 19.29 14.92 29"/><path stroke="#000" d="m160.88-118.33 1.46 10.49-7.69-7.29z"/></g><g class="node" transform="translate(4 184)"><text x="171" y="-13.8" font-size="14" text-anchor="middle">c</text></g><g class="edge" transform="translate(4 184)"><path fill="none" stroke="#000" d="M171-71.7v24.82"/><path stroke="#000" d="m174.5-47.14-3.5 10-3.5-10z"/></g><g class="node" transform="translate(4 184)"><text x="243" y="-13.8" font-size="14" text-anchor="middle">d</text></g><g class="edge" transform="translate(4 184)"><path fill="none" stroke="#000" d="M183.54-76.81c9.4 9.14 22.59 21.96 34.15 33.2"/><path stroke="#000" d="m219.93-46.31 4.73 9.48-9.61-4.46z"/></g></g>
</svg>
<p>Here’s what the decision dag looks like:</p>
<svg width="889pt" viewBox="0 0 888.88 3259.5" style="max-width:100%;">
<g class="graph" transform="translate(4 3255.5)"><path fill="#fff" d="M-4 4v-3259.5h888.88V4z"/><g class="node"><path fill="none" stroke="#000" d="M65.38-3251.5 0-3233.5l65.38 18 65.39-18z"/><text x="65.38" y="-3228.45" font-family="Times,serif" font-size="14" text-anchor="middle">(node? n)</text></g><g class="node"><path fill="none" stroke="#000" d="M475.26-3163H275.51v36h199.75z"/><text x="375.38" y="-3139.95" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nc1) (node-colour n) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M96.81-3223.73c47.73 13.32 139.82 39.01 205.03 57.21"/><path stroke="#000" d="m302.72-3169.91 8.69 6.06-10.57.68z"/><text x="252.27" y="-3184.2" font-family="Times,serif" font-size="14" text-anchor="middle">T</text></g><g class="node"><ellipse cx="782.38" cy="-964" fill="none" stroke="#000" rx="80.01" ry="18"/><text x="782.38" y="-958.95" font-family="Times,serif" font-size="14" text-anchor="middle">(already-balanced)</text></g><g class="edge"><path fill="none" stroke="#000" d="M60.58-3216.4c-4.71 17.25-11.2 45.51-11.2 70.4v2094.5c0 70.2 558.33 60.87 628 69.5 10.41 1.29 21.36 2.84 32.11 4.48"/><path stroke="#000" d="m709.75-981.02 9.34 5-10.42 1.91z"/><text x="53.13" y="-2111.2" font-family="Times,serif" font-size="14" text-anchor="middle">F</text></g><g class="node"><path fill="none" stroke="#000" d="m574.38-3090-114.6 18 114.6 18 114.61-18z"/><text x="574.38" y="-3066.95" font-family="Times,serif" font-size="14" text-anchor="middle">(equal? nc1 'black)</text></g><g class="edge"><path fill="none" stroke="#000" d="M424.32-3126.54c32.22 11.5 74.08 26.43 105.4 37.6"/><path stroke="#000" d="m530.65-3092.32 8.25 6.66-10.6-.07z"/></g><g class="edge"><path fill="none" stroke="#000" d="M645.24-3064.7c88.42 9.86 228.14 32.92 228.14 80.2v1933c0 28.42-23.6 50.33-46.76 64.98"/><path stroke="#000" d="m828.47-983.55-10.38 2.1 6.81-8.12z"/><text x="877.13" y="-2022.7" font-family="Times,serif" font-size="14" text-anchor="middle">F</text></g><g class="node"><path fill="none" stroke="#000" d="M664.88-3001.5h-181v36h181z"/><text x="574.38" y="-2978.45" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nl1) (node-left n) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M574.38-3053.5v40.07"/><path stroke="#000" d="m577.88-3013.49-3.5 10-3.5-10z"/><text x="578.51" y="-3022.7" font-family="Times,serif" font-size="14" text-anchor="middle">T</text></g><g class="node"><path fill="none" stroke="#000" d="m574.38-2928.5-75.51 18 75.51 18 75.52-18z"/><text x="574.38" y="-2905.45" font-family="Times,serif" font-size="14" text-anchor="middle">(node? nl1)</text></g><g class="edge"><path fill="none" stroke="#000" d="M574.38-2965.31v25.27"/><path stroke="#000" d="m577.88-2940.04-3.5 10-3.5-10z"/></g><g class="node"><path fill="none" stroke="#000" d="M613.38-2840h-214v36h214z"/><text x="506.38" y="-2816.95" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nlc1) (node-colour nl1) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M562.85-2894.83c-9.79 12.46-24.08 30.63-35.83 45.58"/><path stroke="#000" d="m529.96-2847.32-8.94 5.7 3.43-10.03z"/><text x="550.6" y="-2861.2" font-family="Times,serif" font-size="14" text-anchor="middle">T</text></g><g class="node"><path fill="none" stroke="#000" d="M698.01-2001.5H508.76v36h189.25z"/><text x="603.38" y="-1978.45" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nr1) (node-right n) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M593.4-2896.64c19.81 15.1 47.98 42.11 47.98 73.64v752c0 21.33-9.8 43.3-19.44 59.77"/><path stroke="#000" d="m625.09-2009.68-8.25 6.65 2.31-10.35z"/><text x="645.13" y="-2449.7" font-family="Times,serif" font-size="14" text-anchor="middle">F</text></g><g class="node"><path fill="none" stroke="#000" d="m506.38-2767-106.64 18 106.64 18 106.64-18z"/><text x="506.38" y="-2743.95" font-family="Times,serif" font-size="14" text-anchor="middle">(equal? nlc1 'red)</text></g><g class="edge"><path fill="none" stroke="#000" d="M506.38-2803.81v25.27"/><path stroke="#000" d="m509.88-2778.54-3.5 10-3.5-10z"/></g><g class="node"><path fill="none" stroke="#000" d="m685.38-1928.5-76.24 18 76.24 18 76.24-18z"/><text x="685.38" y="-1905.45" font-family="Times,serif" font-size="14" text-anchor="middle">(node? nr1)</text></g><g class="edge"><path fill="none" stroke="#000" d="M623.23-1965.31c11.33 9.81 25.63 22.19 37.66 32.6"/><path stroke="#000" d="m663.14-1935.39 5.27 9.19-9.85-3.9z"/></g><g class="edge"><path fill="none" stroke="#000" d="M537.34-2735.73c28.39 13.52 66.04 38.24 66.04 74.23v648.46"/><path stroke="#000" d="m606.88-2013.25-3.5 10-3.5-10z"/><text x="607.13" y="-2361.2" font-family="Times,serif" font-size="14" text-anchor="middle">F</text></g><g class="node"><path fill="none" stroke="#000" d="M570.01-2678.5H374.76v36h195.25z"/><text x="472.38" y="-2655.45" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nll1) (node-left nl1) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M499.99-2731.73c-4.68 11.89-11.13 28.32-16.63 42.3"/><path stroke="#000" d="m486.69-2688.32-6.92 8.03.4-10.59z"/><text x="496.55" y="-2699.7" font-family="Times,serif" font-size="14" text-anchor="middle">T</text></g><g class="node"><path fill="none" stroke="#000" d="m470.38-2605.5-79.13 18 79.13 18 79.14-18z"/><text x="470.38" y="-2582.45" font-family="Times,serif" font-size="14" text-anchor="middle">(node? nll1)</text></g><g class="edge"><path fill="none" stroke="#000" d="M471.9-2642.31c-.22 7.67-.48 16.92-.72 25.57"/><path stroke="#000" d="m474.69-2616.94-3.78 9.9-3.22-10.1z"/></g><g class="node"><path fill="none" stroke="#000" d="M442.13-2517h-221.5v36h221.5z"/><text x="331.38" y="-2493.95" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nllc1) (node-colour nll1) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M449.87-2573.74c-21.22 13.21-54.86 34.14-80.88 50.33"/><path stroke="#000" d="m371.06-2520.57-10.34 2.31 6.64-8.25z"/><text x="417.46" y="-2538.2" font-family="Times,serif" font-size="14" text-anchor="middle">T</text></g><g class="node"><path fill="none" stroke="#000" d="M562.13-2340h-203.5v36h203.5z"/><text x="460.38" y="-2316.95" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nlr1) (node-right nl1) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M469.73-2569.21c-1.69 44.39-6.18 162.72-8.26 217.56"/><path stroke="#000" d="m464.98-2351.73-3.88 9.86-3.12-10.12z"/><text x="469.36" y="-2449.7" font-family="Times,serif" font-size="14" text-anchor="middle">F</text></g><g class="node"><path fill="none" stroke="#000" d="m331.38-2428.5-110.26 18 110.26 18 110.26-18z"/><text x="331.38" y="-2405.45" font-family="Times,serif" font-size="14" text-anchor="middle">(equal? nllc1 'red)</text></g><g class="edge"><path fill="none" stroke="#000" d="M331.38-2480.91v40.89"/><path stroke="#000" d="m334.88-2440.36-3.5 10-3.5-10z"/></g><g class="node"><path fill="none" stroke="#000" d="m460.38-2267-79.86 18 79.86 18 79.86-18z"/><text x="460.38" y="-2243.95" font-family="Times,serif" font-size="14" text-anchor="middle">(node? nlr1)</text></g><g class="edge"><path fill="none" stroke="#000" d="M460.38-2303.81v25.27"/><path stroke="#000" d="m463.88-2278.54-3.5 10-3.5-10z"/></g><g class="edge"><path fill="none" stroke="#000" d="M352.11-2395.6c19.66 13.18 49.58 33.24 72.99 48.94"/><path stroke="#000" d="m426.75-2349.77 6.36 8.48-10.26-2.66z"/><text x="411.19" y="-2361.2" font-family="Times,serif" font-size="14" text-anchor="middle">F</text></g><g class="node"><path fill="none" stroke="#000" d="M333.76-2340H131.01v36h202.75z"/><text x="232.38" y="-2316.95" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nlll1) (node-left nll1) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M314.59-2394.83c-14.66 12.81-36.24 31.67-53.61 46.85"/><path stroke="#000" d="m263.31-2345.37-9.83 3.94 5.22-9.21z"/><text x="294.88" y="-2361.2" font-family="Times,serif" font-size="14" text-anchor="middle">T</text></g><g class="node"><path fill="none" stroke="#000" d="M272.26-2267H162.51v36h109.75z"/><text x="217.38" y="-2243.95" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((a nlll1)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M228.75-2303.81c-1.62 7.67-3.57 16.92-5.4 25.57"/><path stroke="#000" d="m226.79-2277.57-5.5 9.06-1.35-10.51z"/></g><g class="node"><path fill="none" stroke="#000" d="M294.88-2178.5h-217v36h217z"/><text x="186.38" y="-2155.45" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nllv1) (node-value nll1) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M211.26-2230.91c-4.22 11.77-9.92 27.68-14.8 41.3"/><path stroke="#000" d="m199.78-2188.51-6.67 8.24.08-10.6z"/></g><g class="node"><path fill="none" stroke="#000" d="M242.13-2090h-113.5v36h113.5z"/><text x="185.38" y="-2066.95" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((x nllv1)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M186.19-2142.41c-.14 11.65-.32 27.36-.48 40.89"/><path stroke="#000" d="m189.22-2101.82-3.62 9.96-3.38-10.04z"/></g><g class="node"><path fill="none" stroke="#000" d="M289.88-2001.5h-211v36h211z"/><text x="184.38" y="-1978.45" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nllr1) (node-right nll1) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M185.19-2053.91c-.14 11.65-.32 27.36-.48 40.89"/><path stroke="#000" d="m188.22-2013.32-3.62 9.96-3.38-10.04z"/></g><g class="node"><path fill="none" stroke="#000" d="M239.01-1928.5H127.76v36h111.25z"/><text x="183.38" y="-1905.45" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((b nllr1)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M184.14-1965.31c-.11 7.58-.23 16.71-.35 25.27"/><path stroke="#000" d="m187.29-1939.99-3.65 9.95-3.35-10.05z"/></g><g class="node"><path fill="none" stroke="#000" d="M288.13-1840H78.63v36h209.5z"/><text x="183.38" y="-1816.95" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nlv1) (node-value nl1) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M183.38-1892.41v40.89"/><path stroke="#000" d="m186.88-1851.86-3.5 10-3.5-10z"/></g><g class="node"><path fill="none" stroke="#000" d="M236.26-1767H126.51v36h109.75z"/><text x="181.38" y="-1743.95" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((y nlv1)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M182.9-1803.81c-.22 7.67-.48 16.92-.72 25.57"/><path stroke="#000" d="m185.69-1778.44-3.78 9.9-3.22-10.1z"/></g><g class="node"><path fill="none" stroke="#000" d="M282.13-1678.5H78.63v36h203.5z"/><text x="180.38" y="-1655.45" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nlr1) (node-right nl1) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M181.19-1730.91c-.14 11.65-.32 27.36-.48 40.89"/><path stroke="#000" d="m184.22-1690.32-3.62 9.96-3.38-10.04z"/></g><g class="node"><path fill="none" stroke="#000" d="M231.76-1551.5H125.01v36h106.75z"/><text x="178.38" y="-1528.45" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((c nlr1)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M180.11-1642.12c-.33 20.43-.88 54.63-1.27 79"/><path stroke="#000" d="m182.34-1563.19-3.66 9.94-3.34-10.05z"/></g><g class="node"><path fill="none" stroke="#000" d="M275.01-1247.5H79.76v36h195.25z"/><text x="177.38" y="-1224.45" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nv1) (node-value n) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M178.09-1515.11c-.29 19.27-.71 51.29-.71 78.86v177.05"/><path stroke="#000" d="m180.88-1259.23-3.5 10-3.5-10z"/></g><g class="node"><path fill="none" stroke="#000" d="M227.01-1159H121.76v36h105.25z"/><text x="174.38" y="-1135.95" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((z nv1)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M176.79-1211.41c-.4 11.65-.95 27.36-1.42 40.89"/><path stroke="#000" d="m178.88-1170.74-3.84 9.88-3.15-10.12z"/></g><g class="node"><path fill="none" stroke="#000" d="M267.01-1070.5H77.76v36h189.25z"/><text x="172.38" y="-1047.45" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nr1) (node-right n) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M173.99-1122.91c-.27 11.65-.63 27.36-.95 40.89"/><path stroke="#000" d="m176.55-1082.28-3.73 9.92-3.27-10.08z"/></g><g class="node"><path fill="none" stroke="#000" d="M253.26-928H149.51v36h103.75z"/><text x="201.38" y="-904.95" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((d nr1)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M175.94-1034.27c4.87 23.59 13.68 66.27 19.54 94.69"/><path stroke="#000" d="m198.9-940.35-1.41 10.5-5.45-9.09z"/></g><g class="node"><ellipse cx="387.38" cy="-18" fill="none" stroke="#000" rx="106.11" ry="18"/><text x="387.38" y="-12.95" font-family="Times,serif" font-size="14" text-anchor="middle">(do-balance a x b y c z d)</text></g><g class="edge"><path fill="none" stroke="#000" d="M214.15-891.58c11.38 17.27 26.23 44.93 26.23 71.58v730c0 29.66 24.57 46.93 53.61 56.99"/><path stroke="#000" d="m294.91-36.39 8.51 6.32-10.59.37z"/></g><g class="edge"><path fill="none" stroke="#000" d="M426.11-2238.25c-28.06 10-65.91 28.58-83.73 59.75-10.66 18.66-23.7 89.66 1 124.5 25.99 36.64 51.82 22.14 95 34.5 19.36 5.54 40.23 10.63 60.39 15.11"/><path stroke="#000" d="m499.39-2007.84 9.02 5.55-10.52 1.29z"/><text x="333.44" y="-2111.2" font-family="Times,serif" font-size="14" text-anchor="middle">F</text></g><g class="node"><path fill="none" stroke="#000" d="M574.88-2178.5h-223v36h223z"/><text x="463.38" y="-2155.45" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nlrc2) (node-colour nlr1) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M460.98-2230.91c.4 11.65.95 27.36 1.41 40.89"/><path stroke="#000" d="m465.88-2190.48-3.15 10.12-3.85-9.88z"/><text x="466.28" y="-2199.7" font-family="Times,serif" font-size="14" text-anchor="middle">T</text></g><g class="node"><path fill="none" stroke="#000" d="m463.38-2090-110.98 18 110.98 18 110.99-18z"/><text x="463.38" y="-2066.95" font-family="Times,serif" font-size="14" text-anchor="middle">(equal? nlrc2 'red)</text></g><g class="edge"><path fill="none" stroke="#000" d="M463.38-2142.41v40.89"/><path stroke="#000" d="m466.88-2101.86-3.5 10-3.5-10z"/></g><g class="edge"><path fill="none" stroke="#000" d="M485.57-2057.29c21.44 13.24 54.34 33.57 79.94 49.39"/><path stroke="#000" d="m567.31-2010.9 6.67 8.23-10.35-2.28z"/><text x="549.67" y="-2022.7" font-family="Times,serif" font-size="14" text-anchor="middle">F</text></g><g class="node"><path fill="none" stroke="#000" d="M480.38-2001.5h-106v36h106z"/><text x="427.38" y="-1978.45" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((a nll1)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M456.61-2054.73c-4.95 11.89-11.78 28.32-17.6 42.3"/><path stroke="#000" d="m442.27-2011.17-7.07 7.89.61-10.58z"/><text x="452.73" y="-2022.7" font-family="Times,serif" font-size="14" text-anchor="middle">T</text></g><g class="node"><path fill="none" stroke="#000" d="M504.13-1928.5h-209.5v36h209.5z"/><text x="399.38" y="-1905.45" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nlv1) (node-value nl1) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M420.61-1965.31c-3.1 7.85-6.84 17.34-10.32 26.15"/><path stroke="#000" d="m413.58-1937.96-6.93 8.02.42-10.58z"/></g><g class="node"><path fill="none" stroke="#000" d="M416.26-1840H306.51v36h109.75z"/><text x="361.38" y="-1816.95" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((x nlv1)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M391.88-1892.41c-5.23 11.89-12.3 28-18.33 41.71"/><path stroke="#000" d="m376.84-1849.48-7.23 7.74.82-10.56z"/></g><g class="node"><path fill="none" stroke="#000" d="M461.51-1767H257.26v36h204.25z"/><text x="359.38" y="-1743.95" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nlrl2) (node-left nlr1) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M360.9-1803.81c-.22 7.67-.48 16.92-.72 25.57"/><path stroke="#000" d="m363.69-1778.44-3.78 9.9-3.22-10.1z"/></g><g class="node"><path fill="none" stroke="#000" d="M413.01-1678.5H301.76v36h111.25z"/><text x="357.38" y="-1655.45" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((b nlrl2)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M358.99-1730.91c-.27 11.65-.63 27.36-.95 40.89"/><path stroke="#000" d="m361.55-1690.28-3.73 9.92-3.27-10.08z"/></g><g class="node"><path fill="none" stroke="#000" d="M442.63-1605.5h-218.5v36h218.5z"/><text x="333.38" y="-1582.45" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nlrv2) (node-value nlr1) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M351.57-1642.31c-2.65 7.85-5.86 17.34-8.84 26.15"/><path stroke="#000" d="m346.14-1615.32-6.52 8.36-.11-10.6z"/></g><g class="node"><path fill="none" stroke="#000" d="M376.51-1497.5H262.26v36h114.25z"/><text x="319.38" y="-1474.45" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((y nlrv2)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M331.13-1569.47c-2.15 16.28-5.43 41.15-7.98 60.45"/><path stroke="#000" d="m326.65-1508.8-4.78 9.46-2.16-10.37z"/></g><g class="node"><path fill="none" stroke="#000" d="M417.63-1409h-212.5v36h212.5z"/><text x="311.38" y="-1385.95" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nlrr2) (node-right nlr1) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M317.8-1461.41c-1.07 11.65-2.53 27.36-3.78 40.89"/><path stroke="#000" d="m317.53-1420.49-4.4 9.63-2.57-10.28z"/></g><g class="node"><path fill="none" stroke="#000" d="M352.01-1320.5H240.76v36h111.25z"/><text x="296.38" y="-1297.45" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((c nlrr2)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M308.42-1372.91c-2.02 11.65-4.74 27.36-7.09 40.89"/><path stroke="#000" d="m304.81-1331.6-5.16 9.26-1.74-10.45z"/></g><g class="edge"><path fill="none" stroke="#000" d="M267.27-1284.13c-15.41 9.2-34.55 20.61-51.2 30.55"/><path stroke="#000" d="m218.29-1250.83-10.38 2.12 6.79-8.13z"/></g><g class="edge"><path fill="none" stroke="#000" d="M730.8-1902.86c44.14 9.32 104.58 31.01 104.58 79.86v771.5c0 23.18-14.01 45.39-27.62 61.5"/><path stroke="#000" d="m810.62-987.94-9.28 5.11 4.07-9.79z"/><text x="839.13" y="-1430.2" font-family="Times,serif" font-size="14" text-anchor="middle">F</text></g><g class="node"><path fill="none" stroke="#000" d="M793.13-1840h-215.5v36h215.5z"/><text x="685.38" y="-1816.95" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nrc3) (node-colour nr1) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M685.38-1892v40.07"/><path stroke="#000" d="m688.88-1851.99-3.5 10-3.5-10z"/><text x="689.51" y="-1861.2" font-family="Times,serif" font-size="14" text-anchor="middle">T</text></g><g class="node"><path fill="none" stroke="#000" d="m685.38-1767-107.36 18 107.36 18 107.37-18z"/><text x="685.38" y="-1743.95" font-family="Times,serif" font-size="14" text-anchor="middle">(equal? nrc3 'red)</text></g><g class="edge"><path fill="none" stroke="#000" d="M685.38-1803.81v25.27"/><path stroke="#000" d="m688.88-1778.54-3.5 10-3.5-10z"/></g><g class="edge"><path fill="none" stroke="#000" d="M722.06-1736.74c32.66 12.57 75.32 36.42 75.32 75.24v610c0 19.69-3.82 41.54-7.6 58.35"/><path stroke="#000" d="m793.29-992.79-5.73 8.91-1.08-10.54z"/><text x="801.13" y="-1341.7" font-family="Times,serif" font-size="14" text-anchor="middle">F</text></g><g class="node"><path fill="none" stroke="#000" d="M769.76-1678.5H573.01v36h196.75z"/><text x="671.38" y="-1655.45" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nrl3) (node-left nr1) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M682.68-1731.32c-1.89 11.69-4.47 27.62-6.68 41.31"/><path stroke="#000" d="m679.47-1689.53-5.06 9.32-1.85-10.43z"/><text x="683.76" y="-1699.7" font-family="Times,serif" font-size="14" text-anchor="middle">T</text></g><g class="node"><path fill="none" stroke="#000" d="m671.38-1605.5-79.86 18 79.86 18 79.86-18z"/><text x="671.38" y="-1582.45" font-family="Times,serif" font-size="14" text-anchor="middle">(node? nrl3)</text></g><g class="edge"><path fill="none" stroke="#000" d="M671.38-1642.31v25.27"/><path stroke="#000" d="m674.88-1617.04-3.5 10-3.5-10z"/></g><g class="node"><path fill="none" stroke="#000" d="M656.88-1497.5h-223v36h223z"/><text x="545.38" y="-1474.45" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nrlc3) (node-colour nrl3) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M655.17-1572.86c-20.25 17.03-55.26 46.49-80.3 67.56"/><path stroke="#000" d="m577.2-1502.69-9.9 3.76 5.4-9.12z"/><text x="631.16" y="-1528.45" font-family="Times,serif" font-size="14" text-anchor="middle">T</text></g><g class="node"><path fill="none" stroke="#000" d="M766.88-1320.5h-205v36h205z"/><text x="664.38" y="-1297.45" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nrr3) (node-right nr1) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M671.01-1569.34c-.79 36.25-2.72 123.32-4.63 196.34-.35 13.44-.77 28.35-1.14 40.89"/><path stroke="#000" d="m668.75-1332.33-3.79 9.89-3.21-10.09z"/><text x="671.88" y="-1430.2" font-family="Times,serif" font-size="14" text-anchor="middle">F</text></g><g class="node"><path fill="none" stroke="#000" d="m546.38-1409-110.98 18 110.98 18 110.99-18z"/><text x="546.38" y="-1385.95" font-family="Times,serif" font-size="14" text-anchor="middle">(equal? nrlc3 'red)</text></g><g class="edge"><path fill="none" stroke="#000" d="M545.58-1461.41c.14 11.65.32 27.36.47 40.89"/><path stroke="#000" d="m549.55-1420.9-3.38 10.04-3.62-9.96z"/></g><g class="node"><path fill="none" stroke="#000" d="m664.38-1247.5-80.58 18 80.58 18 80.59-18z"/><text x="664.38" y="-1224.45" font-family="Times,serif" font-size="14" text-anchor="middle">(node? nrr3)</text></g><g class="edge"><path fill="none" stroke="#000" d="M664.38-1284.31v25.27"/><path stroke="#000" d="m667.88-1259.04-3.5 10-3.5-10z"/></g><g class="edge"><path fill="none" stroke="#000" d="M565.87-1375.72c17.82 13.07 44.55 32.66 65.67 48.15"/><path stroke="#000" d="m633.59-1330.42 5.99 8.74-10.13-3.09z"/><text x="619.7" y="-1341.7" font-family="Times,serif" font-size="14" text-anchor="middle">F</text></g><g class="node"><path fill="none" stroke="#000" d="M483.51-1320.5H381.26v36h102.25z"/><text x="432.38" y="-1297.45" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((a nl1)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M527.56-1375.72c-17.22 13.07-43.04 32.66-63.45 48.15"/><path stroke="#000" d="m466.45-1324.96-10.08 3.26 5.85-8.83z"/><text x="503.72" y="-1341.7" font-family="Times,serif" font-size="14" text-anchor="middle">T</text></g><g class="node"><path fill="none" stroke="#000" d="M523.01-1247.5H327.76v36h195.25z"/><text x="425.38" y="-1224.45" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nv1) (node-value n) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M430.69-1284.31c-.76 7.67-1.67 16.92-2.52 25.57"/><path stroke="#000" d="m431.68-1258.64-4.47 9.6-2.5-10.29z"/></g><g class="node"><path fill="none" stroke="#000" d="M453.38-1159h-106v36h106z"/><text x="400.38" y="-1135.95" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((x nv1)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M420.44-1211.41c-3.4 11.77-8 27.68-11.93 41.3"/><path stroke="#000" d="m411.96-1169.44-6.14 8.64-.59-10.58z"/></g><g class="node"><path fill="none" stroke="#000" d="M489.51-1070.5H285.26v36h204.25z"/><text x="387.38" y="-1047.45" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nrll3) (node-left nrl3) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M397.82-1122.91c-1.76 11.65-4.12 27.36-6.15 40.89"/><path stroke="#000" d="m395.16-1081.72-4.94 9.37-1.98-10.41z"/></g><g class="node"><path fill="none" stroke="#000" d="M443.01-982H331.76v36h111.25z"/><text x="387.38" y="-958.95" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((b nrll3)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M387.38-1034.41v40.89"/><path stroke="#000" d="m390.88-993.86-3.5 10-3.5-10z"/></g><g class="node"><path fill="none" stroke="#000" d="M496.63-874h-218.5v36h218.5z"/><text x="387.38" y="-850.95" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nrlv3) (node-value nrl3) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M387.38-945.97v60.18"/><path stroke="#000" d="m390.88-885.85-3.5 10-3.5-10z"/></g><g class="node"><path fill="none" stroke="#000" d="M444.51-801H330.26v36h114.25z"/><text x="387.38" y="-777.95" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((y nrlv3)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M387.38-837.81v25.27"/><path stroke="#000" d="m390.88-812.54-3.5 10-3.5-10z"/></g><g class="node"><path fill="none" stroke="#000" d="M493.63-728h-212.5v36h212.5z"/><text x="387.38" y="-704.95" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nrlr3) (node-right nrl3) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M387.38-764.81v25.27"/><path stroke="#000" d="m390.88-739.54-3.5 10-3.5-10z"/></g><g class="node"><path fill="none" stroke="#000" d="M443.01-655H331.76v36h111.25z"/><text x="387.38" y="-631.95" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((c nrlr3)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M387.38-691.81v25.27"/><path stroke="#000" d="m390.88-666.54-3.5 10-3.5-10z"/></g><g class="node"><path fill="none" stroke="#000" d="M492.88-582h-211v36h211z"/><text x="387.38" y="-558.95" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nrv3) (node-value nr1) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M387.38-618.81v25.27"/><path stroke="#000" d="m390.88-593.54-3.5 10-3.5-10z"/></g><g class="node"><path fill="none" stroke="#000" d="M442.26-509H332.51v36h109.75z"/><text x="387.38" y="-485.95" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((z nrv3)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M387.38-545.81v25.27"/><path stroke="#000" d="m390.88-520.54-3.5 10-3.5-10z"/></g><g class="node"><path fill="none" stroke="#000" d="M489.88-436h-205v36h205z"/><text x="387.38" y="-412.95" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nrr3) (node-right nr1) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M387.38-472.81v25.27"/><path stroke="#000" d="m390.88-447.54-3.5 10-3.5-10z"/></g><g class="node"><path fill="none" stroke="#000" d="M441.51-309H333.26v36h108.25z"/><text x="387.38" y="-285.95" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((d nrr3)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M387.38-399.62v79"/><path stroke="#000" d="m390.88-320.75-3.5 10-3.5-10z"/></g><g class="edge"><path fill="none" stroke="#000" d="M387.38-272.61V-47.7"/><path stroke="#000" d="m390.88-47.73-3.5 10-3.5-10z"/></g><g class="edge"><path fill="none" stroke="#000" d="M684.43-1215.56c17.52 12.39 42.23 32.69 55.95 56.56 30.44 52.96 38.86 125.58 41.17 165.25"/><path stroke="#000" d="m785.04-993.99-3.02 10.15-3.97-9.82z"/><text x="771.11" y="-1091.7" font-family="Times,serif" font-size="14" text-anchor="middle">F</text></g><g class="node"><path fill="none" stroke="#000" d="M731.63-1159h-224.5v36h224.5z"/><text x="619.38" y="-1135.95" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nrrc4) (node-colour nrr3) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="m656.34-1213.04-22.74 43.72"/><path stroke="#000" d="m636.86-1168.01-7.72 7.26 1.51-10.49z"/><text x="650.04" y="-1180.2" font-family="Times,serif" font-size="14" text-anchor="middle">T</text></g><g class="node"><path fill="none" stroke="#000" d="m619.38-1070.5-111.7 18 111.7 18 111.71-18z"/><text x="619.38" y="-1047.45" font-family="Times,serif" font-size="14" text-anchor="middle">(equal? nrrc4 'red)</text></g><g class="edge"><path fill="none" stroke="#000" d="M619.38-1122.91v40.89"/><path stroke="#000" d="m622.88-1082.36-3.5 10-3.5-10z"/></g><g class="edge"><path fill="none" stroke="#000" d="M644.5-1038.17c26.07 13.83 67.25 35.69 97.83 51.91"/><path stroke="#000" d="m743.72-989.48 7.2 7.78-10.48-1.59z"/><text x="719.23" y="-1003.2" font-family="Times,serif" font-size="14" text-anchor="middle">F</text></g><g class="node"><path fill="none" stroke="#000" d="M668.51-982H566.26v36h102.25z"/><text x="617.38" y="-958.95" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((a nl1)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M618.99-1034.41c-.27 11.65-.63 27.36-.95 40.89"/><path stroke="#000" d="m621.55-993.78-3.73 9.92-3.27-10.08z"/><text x="622.69" y="-1003.2" font-family="Times,serif" font-size="14" text-anchor="middle">T</text></g><g class="node"><path fill="none" stroke="#000" d="M712.01-874H516.76v36h195.25z"/><text x="614.38" y="-850.95" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nv1) (node-value n) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M616.9-945.97c-.46 16.21-1.16 40.91-1.7 60.18"/><path stroke="#000" d="m618.7-885.75-3.78 9.9-3.22-10.1z"/></g><g class="node"><path fill="none" stroke="#000" d="M665.38-801h-106v36h106z"/><text x="612.38" y="-777.95" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((x nv1)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M613.9-837.81c-.22 7.67-.48 16.92-.72 25.57"/><path stroke="#000" d="m616.69-812.44-3.78 9.9-3.22-10.1z"/></g><g class="node"><path fill="none" stroke="#000" d="M641.13-728h-107.5v36h107.5z"/><text x="587.38" y="-704.95" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((b nrl3)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M606.33-764.81c-2.76 7.85-6.1 17.34-9.21 26.15"/><path stroke="#000" d="m600.51-737.73-6.63 8.27.02-10.59z"/></g><g class="node"><path fill="none" stroke="#000" d="M680.88-655h-211v36h211z"/><text x="575.38" y="-631.95" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nrv3) (node-value nr1) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M584.48-691.81c-1.3 7.67-2.86 16.92-4.32 25.57"/><path stroke="#000" d="m583.63-665.8-5.12 9.28-1.78-10.44z"/></g><g class="node"><path fill="none" stroke="#000" d="M624.63-582h-110.5v36h110.5z"/><text x="569.38" y="-558.95" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((y nrv3)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M573.93-618.81c-.65 7.67-1.43 16.92-2.16 25.57"/><path stroke="#000" d="m575.28-593.21-4.33 9.67-2.65-10.26z"/></g><g class="node"><path fill="none" stroke="#000" d="M670.26-509H464.51v36h205.75z"/><text x="567.38" y="-485.95" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nrrl4) (node-left nrr3) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M568.9-545.81c-.22 7.67-.48 16.92-.72 25.57"/><path stroke="#000" d="m571.69-520.44-3.78 9.9-3.22-10.1z"/></g><g class="node"><path fill="none" stroke="#000" d="M620.01-436H508.76v36h111.25z"/><text x="564.38" y="-412.95" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((c nrrl4)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M566.66-472.81c-.33 7.67-.72 16.92-1.08 25.57"/><path stroke="#000" d="m569.09-447.38-3.92 9.84-3.08-10.14z"/></g><g class="node"><path fill="none" stroke="#000" d="M654.38-363h-220v36h220z"/><text x="544.38" y="-339.95" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nrrv4) (node-value nrr3) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M559.54-399.81c-2.18 7.76-4.82 17.13-7.28 25.86"/><path stroke="#000" d="m555.67-373.16-6.08 8.67-.66-10.57z"/></g><g class="node"><path fill="none" stroke="#000" d="M588.51-255H474.26v36h114.25z"/><text x="531.38" y="-231.95" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((z nrrv4)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M542.3-326.97c-2 16.28-5.05 41.15-7.42 60.45"/><path stroke="#000" d="m538.39-266.34-4.7 9.5-2.25-10.35z"/></g><g class="node"><path fill="none" stroke="#000" d="M632.38-182h-214v36h214z"/><text x="525.38" y="-158.95" font-family="Times,serif" font-size="14" text-anchor="middle">(receive (nrrr4) (node-right nrr3) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M529.93-218.81c-.65 7.67-1.43 16.92-2.16 25.57"/><path stroke="#000" d="m531.28-193.21-4.33 9.67-2.65-10.26z"/></g><g class="node"><path fill="none" stroke="#000" d="M554.76-109H442.01v36h112.75z"/><text x="498.38" y="-85.95" font-family="Times,serif" font-size="14" text-anchor="middle">(let ((d nrrr4)) …)</text></g><g class="edge"><path fill="none" stroke="#000" d="M518.85-145.81c-2.99 7.85-6.6 17.34-9.95 26.15"/><path stroke="#000" d="m512.22-118.55-6.82 8.11.28-10.59z"/></g><g class="edge"><path fill="none" stroke="#000" d="M471.23-72.63c-14.56 9.31-32.69 20.91-48.35 30.93"/><path stroke="#000" d="m425.08-38.96-10.31 2.44 6.54-8.33z"/></g></g>
</svg>
<p>You can see that nodes are shared (<a href="#:~:text=(receive%20(nlr1)%20(node-right%20nl1)%20%E2%80%A6)">1</a>, <a href="#:~:text=(receive%20(nr1)%20(node-right%20n)%20%E2%80%A6)">2</a>, <a href="#:~:text=(receive%20(nrr3)%20(node-right%20nr1)%20%E2%80%A6)">3</a>, <a href="#:~:text=%28receive%20%28nv1%29%20%28node%2Dvalue%20n%29%20%E2%80%A6%29">4</a>) to reduce the overall size of the graph. This may look big and complicated, but at run time it’s efficient because no test or extraction will ever be repeated on any given code path. There are 13 branch points, <a href="https://compiler.club/compiling-pattern-matching/#:~:text=Below%2C%20you%20can%20see%20the%20decision%20DAG%20(for%20the%20balance%20example)%20resulting%20from%20an%20implementation%20that%20uses%20hash%20consing:">the same number as in Colin James’s independent OCaml implementation of Maranget’s algorithm</a>, a nice confirmation that my implementation is doing the right thing :-)</p>
<p>Can you imagine trying to write out the code for this pattern match by hand? You don’t have to imagine, because <a href="https://github.com/clojure/clojure/blob/345aff14826f375f9809bd8a6b36b6b8b9927100/src/jvm/clojure/lang/PersistentTreeMap.java">Rich Hickey implemented this data structure in bad old Java</a> as part of Clojure. His version is spread all over the place because old-school Java just lacked the features to make writing this function nice. (I’m not even sure new-school Java could do much better.)</p>
<p>For an example where things start to fall down in Scheme, consider <a href="https://pauillac.inria.fr/~maranget/X/TLP/04/machine.html#:~:text=let%C2%A0rec%C2%A0run%C2%A0a%C2%A0s%C2%A0e%C2%A0c">Maranget’s bytecode interpreter</a> for a miniature dialect of ML. In figures 6 and 7 of his 2008 paper – which really ought to be shown as motivating examples on the first page – Maranget showed how a bad and a good decision tree for this can look in OCaml, a statically-typed language.</p>
<p>First of all, here’s roughly what the run function would look like in Scheme:</p>
<div class="sourceCode" id="cb14"><pre class="sourceCode scheme"><code class="sourceCode scheme"><span id="cb14-1"><a href="#cb14-1" aria-hidden="true" tabindex="-1"></a>(<span class="ex">define</span><span class="fu"> </span>(run a s e c)</span>
<span id="cb14-2"><a href="#cb14-2" aria-hidden="true" tabindex="-1"></a>  (match (<span class="kw">values</span> a s c)</span>
<span id="cb14-3"><a href="#cb14-3" aria-hidden="true" tabindex="-1"></a>    ((<span class="op">_</span> <span class="op">_</span> (<span class="kw">cons</span> (ldi i) c))  (case-1))</span>
<span id="cb14-4"><a href="#cb14-4" aria-hidden="true" tabindex="-1"></a>    ((<span class="op">_</span> <span class="op">_</span> (<span class="kw">cons</span> (push) c))   (case-2))</span>
<span id="cb14-5"><a href="#cb14-5" aria-hidden="true" tabindex="-1"></a>    (((<span class="op">?</span> <span class="kw">integer?</span> n2) (val (<span class="op">?</span> <span class="kw">integer?</span> n1)) (<span class="kw">cons</span> (iop op) c))  (case-3))</span>
<span id="cb14-6"><a href="#cb14-6" aria-hidden="true" tabindex="-1"></a>    ((<span class="dv">0</span>            <span class="op">_</span> (<span class="kw">cons</span> (test c2 <span class="op">_</span>) c))  (case-4))</span>
<span id="cb14-7"><a href="#cb14-7" aria-hidden="true" tabindex="-1"></a>    (((<span class="op">?</span> <span class="kw">integer?</span>) <span class="op">_</span> (<span class="kw">cons</span> (test <span class="op">_</span> c3) c))  (case-5))</span>
<span id="cb14-8"><a href="#cb14-8" aria-hidden="true" tabindex="-1"></a>    ((<span class="op">_</span> <span class="op">_</span> (<span class="kw">cons</span> (extend) c))    (case-6))</span>
<span id="cb14-9"><a href="#cb14-9" aria-hidden="true" tabindex="-1"></a>    ((<span class="op">_</span> <span class="op">_</span> (<span class="kw">cons</span> (search k) c))  (case-7))</span>
<span id="cb14-10"><a href="#cb14-10" aria-hidden="true" tabindex="-1"></a>    ((<span class="op">_</span> <span class="op">_</span> (<span class="kw">cons</span> (pushenv) c))   (case-8))</span>
<span id="cb14-11"><a href="#cb14-11" aria-hidden="true" tabindex="-1"></a>    ((<span class="op">_</span> (<span class="kw">cons</span> (env e) s) (<span class="kw">cons</span> (popenv) c)) (case-9))</span>
<span id="cb14-12"><a href="#cb14-12" aria-hidden="true" tabindex="-1"></a>    ((<span class="op">_</span> <span class="op">_</span> (<span class="kw">cons</span> (mkclos cc) c))     (case-10))</span>
<span id="cb14-13"><a href="#cb14-13" aria-hidden="true" tabindex="-1"></a>    ((<span class="op">_</span> <span class="op">_</span> (<span class="kw">cons</span> (mkclosrec cc) c))  (case-11))</span>
<span id="cb14-14"><a href="#cb14-14" aria-hidden="true" tabindex="-1"></a>    (((clo cc ce) (<span class="kw">cons</span> (val v) s) (<span class="kw">cons</span> (apply) c))  (case-12))</span>
<span id="cb14-15"><a href="#cb14-15" aria-hidden="true" tabindex="-1"></a>    ((a (<span class="kw">cons</span> (code c) (<span class="kw">cons</span> (env e) s)) &#39;())         (case-13))</span>
<span id="cb14-16"><a href="#cb14-16" aria-hidden="true" tabindex="-1"></a>    ((a &#39;() &#39;())  (case-14))))</span></code></pre></div>
<p>You can already see that we’re running up against the limits of what it’s sensible to do in Lisp notation here.</p>
<p>What does it look like when compiled? I shared what it looked like in the very first working versions of the decision tree generator above … and today it looks like <a href="/images/2025-11-28-bytecode-machine.png">this</a>. To be sure, there’s improvement there (196 nodes in the December 2024 version, 135 nodes today). But it’s also very far from the nice, elegant dag of just a dozen or so branches which Maranget shows in his paper, even accounting for the fact that his version can switch over many types at once and mine can’t. What gives?</p>
<p>Well, here dynamic typing messes up opportunities to do better. If you trace down the graph from the top, it looks good until you get to the third case. Once the pattern matcher knows that the current instruction (the first element in the list <code>c</code> for code) is an <code>iop</code> but the top of the stack <code>s</code> isn’t a <code>val</code>, it should jump straight to <code>fail</code> because there is no situation in which any of the other patterns can match. But it doesn’t know enough to know that; what if something might match both <code>iop</code> <em>and</em> <code>test</code>, or <code>pushenv</code>? A lot of these nodes are therefore dead, and will never execute except to go a long, slow way to a case that should never happen when this function is actually running anyway. I’ve been meaning to study in details exactly which branches are impossible, and see if it would be possible to do anything at all better here, like allow users to declare which predicates in <code>?</code> are mutually exclusive. (<del>Initial results on that particular feature weren’t exactly encouraging either.</del> <ins>Update, 30 November:</ins> Scratch that! I just tried it again and <a href="/images/2025-11-30-bytecode-machine-with-complementary-predicates.png">it works great</a>. 86 nodes, down from 135. I must have forgotten to clear the cache of pre-compiled code before the last time I tested it, or something. The problem now is that a complete implementation of this would require implementing something like Datalog …) It does do some very basic stuff to avoid generating redundant nodes like this, but not nearly as much as an ML compiler can.</p>
<p>As the folklore predicts, Racket’s backtracking automaton does better here for code size but worse for speed, generating many checks that <code>c</code> is a pair and many separate extractions of its car and cdr. It only does these things once for cases 1 and 2, but again for case 3, again for case 4, again for case 5, and only gets back on a roll again with cases 6, 7, and 8 …</p>
<h2 id="is-all-of-this-worth-it">Is all of this worth it?</h2>
<p>Last of all, here’s the hand-wringing section where I ask whether I should have bothered with any of this!</p>
<p>In 2000 Kevin Scott and Norman Ramsey wrote a draft paper asking <a href="https://www.cs.tufts.edu/~nr/cs257/archive/norman-ramsey/match.pdf">‘When Do Match-Compilation Heuristics Matter?’</a>; their answer is ‘almost never’. Out of 18 program benchmarks, only five were affected in decision tree size by the use of different heuristics, and three of those were made <em>worse</em> by choosing heuristics other than simple left to right selection of patterns. More encouraging is that 13 of the 18 benchmarks were artificial benchmark programs, but one of the five where heuristics did make some difference was a real-world program (the SML/NJ compiler).</p>
<p>Maranget’s results measuring heuristics may be more representative because he created a corpus of match expressions to test on specifically designed to represent a wide range of sizes and matching techniques; and because he actually used his own decision tree generation algorithm, the one extensible-match’s is based on. He finds a larger difference than Scott and Ramsey – about 20% improvement in code size and 10% improvement in average path length for any individual heuristic, compared to just naïvely picking the leftmost pattern in each row every time. As a reflection on how Maranget’s corpus might not be exactly representative, though, the heuristics only score about 15% better for code size and about 3% better for average path length than naïvely picking the <em>rightmost</em> pattern in each row.</p>
<p>Still, these are fairly slim pickings. As mentioned above, Guile’s optimizer is already capable of taking a naïve pattern match compilation and doing optimizations equivalent to generating a backtracking automaton that always picks the leftmost pattern for each operation; basically on the bytecode machine example it would probably do about as well as Racket’s match compiler, even without any help from the macro. The benchmarks by Scott and Ramsey suggest this should be good enough for almost every real use case. If Guile ever ships SRFI 262 in the standard distribution, it might well not be worth the extra weight in code to include a heuristic-driven decision tree generator; maybe that version should go back to just generating the naïve kind of compilation shown in the very first example above, and should let Guile’s own optimizer do the work. Changing strategy to the automaton style in that case also seem like the right thing, given that any version shipped with Guile would probably also end up in <a href="https://www.spritely.institute/hoot/">Hoot</a>, which has its own reasons for optimizing code size. For other Scheme implementations which don’t (yet) do the online common subexpression elimination pass Andy Wingo developed, the winnings will definitely be bigger – although still diminishing if online CSE becomes more common.</p>
<h2 id="next-time">Next time</h2>
<p>Whew, this was a long one. Next time will be a comparatively short conclusion, on the code generator.</p>
<section id="footnotes" class="footnotes footnotes-end-of-document" role="doc-endnotes">
<hr />
<ol>
<li id="fn1"><p>Especially considering personal matters got in the way of completing this article promptly. Sorry about that!<a href="#fnref1" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn2"><p>A transport/railway simulation game like Transport Tycoon that ran in a text terminal. That makes it sound a lot cooler (and more fun, and more functional) than it actually was.</p>
<p>If you want to learn how to do pathfinding in a game these days, that tutorial still isn’t bad, but you might have more fun with <a href="https://www.redblobgames.com/pathfinding/a-star/">Amit Patel’s interactive walkthrough</a> including other graph search algorithms.<a href="#fnref2" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn3"><p>This example is from Philip Wadler’s chapter ‘Efficient Compilation of Pattern Matching’ in Simon Peyton Jones’s book <a href="https://simon.peytonjones.org/assets/pdfs/slpj-book-1987-2up-searchable.pdf"><cite>The Implementation of Functional Programming Languages</cite></a>, renamed after <a href="https://man.freebsd.org/cgi/man.cgi?query=uniq&amp;apropos=0&amp;sektion=1">a Unix command line utility</a> which hackers are probably at least passingly familiar with, and with an explicit comparison predicate argument because Scheme is monomorphic. The rest of this article has absolutely nothing to do with the contents of that chapter.<a href="#fnref3" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn4"><p>In 2016 Sam Tobin-Hochstadt held a <a href="https://www.youtube.com/watch?v=IikGK8XP5_Q#t=2m30s">public seminar on YouTube</a> explaining Racket’s implementation.<a href="#fnref4" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn5"><p>Actually it’s five vs ten because I’m still omitting the <code>seq-pattern</code> and its decision tree equivalent <code>dt-seq</code> here.<a href="#fnref5" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn6"><p>This is actually a requirement of the semantics of SRFI 262: all implementations have to use something like f as their first ‘heuristic’. This isn’t necessary in ML because the type testing functions the compiler notionally generates calls to are internal to the ML implementation and known to have universal domain; in SRFI 262 it’s possible to use a previous row as a type check that a procedure in a subsequent row will be safe to call.<a href="#fnref6" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn7"><p><a href="https://spidermonkey.dev/blog/2025/10/28/iongraph-web.html">Nicer, home-spun solutions are possible</a> at the cost of more work.<a href="#fnref7" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
</ol>
</section>]]></summary>
</entry>
<entry>
    <title>Tour of a pattern matcher: core patterns and the AST</title>
    <link href="https://crumbles.blog/posts/2025-11-15-extensible-match-ast.html" />
    <id>https://crumbles.blog/posts/2025-11-15-extensible-match-ast.html</id>
    <published>2025-11-15T13:35:12Z</published>
    <updated>2025-11-15T13:35:12Z</updated>
    <summary type="html"><![CDATA[<p>Alright! <a href="/posts/2025-11-09-extensible-match-front-end.html">Last time</a> we looked at the mouth of extensible-match and how it chews up patterns in order to make them digestible. Before we take a look into the stomach,<a href="#fn1" class="footnote-ref" id="fnref1" role="doc-noteref"><sup>1</sup></a> we should look at what that chewed-up form of patterns looks like. This is an episode with a bit less <em>how</em> and a lot more <em>what</em> than the last one. I’ll explain and justify the core pattern structure we just got out of the expansion process, how and why it’s different from the AST that we eventually like to work on, but also one further transformation on that AST that makes it easier for the rest of the system to digest.</p>
<p>We’re still in the <code>(extensible-match match)</code> library, which we’ll keep dipping back up into because it contains <a href="https://codeberg.org/dpk/extensible-match/src/commit/6a38d3a346413dadfae50bdb98dd9922573bf713/extensible-match/match.sls#L75-L102">this long chain of procedure calls</a> which actually co-ordinates the whole of the rest of the process of compilation.</p>
<h2 id="subject-variables-vs-pattern-variables">Subject variables vs pattern variables</h2>
<p>The idea of a pattern variable is simple, and is a concept exposed to the user. In a pattern like <code>(cons x '())</code>, <code>x</code> is a pattern variable. This pattern deconstructs a pair which could previously have been constructed by the same code used as an expression: the expression form takes a variable and makes it the only element of a 1-list; the pattern form takes a 1-list and puts its element in a variable. This is the essence of the equational reasoning property which makes pattern matching such a nice style to work with and think in.</p>
<p>Subject variables are pattern variables’ twin from the dark side. In the above example, there’ll be three subject variables: one for the pair itself; one for its car, and one for its cdr. The upshot is that the pattern matcher doesn’t generate code like <code>(if (equal? (cdr p) '()) ...)</code>; it will always extract the thing it’s interested in to a variable first: <code>(let ((g1234 (cdr p))) (if (equal? g1234 '()) ...))</code>. This simplifies things because it means that the kind of thing we’re testing against is always a simple variable expression no matter what the nesting level of patterns is.</p>
<p>(Obiter dictum: this doesn’t affect the efficiency of the final generated code; it anticipates the Scheme compiler’s own pass which will un-nest expressions, namely <a href="https://matt.might.net/articles/cps-conversion/">CPS</a> or <a href="https://matt.might.net/articles/a-normalization/">ANF</a> conversion.)</p>
<p>Subject variables are always generated identifiers; their precise names are only interesting to the internals of the pattern matcher. I mention them here because they’re important for understanding the structure of expanded patterns and the first few stages of compilation we’ll look at today. The first thing that <code>%core-match-lambda</code> does to patterns, in fact, is to generate subject variables for each of the values it will be examining. (Recall that <code>%core-match-lambda</code> has multiple sets of patterns, but each such set has to match the same number of values. The values for all clauses get the same subject variables.) Although these subject variables are the entry-point for the pattern match, this is in fact the last set of new subject variables generated, because all of the others (in the example above, the ones for the car and cdr of the pair) are generated during expansion.</p>
<h2 id="core-patterns">Core patterns</h2>
<p>Core patterns are defined in <a href="https://codeberg.org/dpk/extensible-match/src/commit/924ebeb58653ab8b77a51b45c4de29fb32f88dee/extensible-match/core-pattern.sls"><code>(extensible-match core-pattern)</code></a>.</p>
<p>Behold, a grammar:</p>
<table>
<tr>
<td rowspan=11 style="vertical-align: top;">
⟨core pattern⟩
<td>
::=
<td>
<code>(core:wildcard)</code>
<tr>
<td>
|
<td>
<code>(core:var</code> ⟨identifier⟩<code>)</code>
<tr>
<td>
|
<td>
<code>(core:quote</code> ⟨datum⟩<code>)</code>
<tr>
<td>
|
<td>
<code>(core:and</code> ⟨core pattern<sub>1</sub>⟩ ⟨core pattern<sub>2</sub>⟩<code>)</code>
<tr>
<td>
|
<td>
<code>(core:or</code> ⟨core pattern<sub>1</sub>⟩ ⟨core pattern<sub>2</sub>⟩<code>)</code>
<tr>
<td>
|
<td>
<code>(core:not</code> ⟨core pattern⟩<code>)</code>
<tr>
<td>
|
<td>
<code>(core:row</code> ⟨core pattern⟩ …<code>)</code>
<tr>
<td>
|
<td>
<code>(core:?</code> ⟨expression⟩<code>)</code>
<tr>
<td>
|
<td>
<code>(core:apply</code> ⟨expression⟩ <code>(</code>⟨identifier⟩ …<code>)</code> ⟨core pattern⟩<code>)</code>
<tr>
<td>
|
<td>
<code>(core:subject</code> ⟨identifier⟩ ⟨core pattern⟩<code>)</code>
<tr>
<td>
|
<td>
<code>(core:seq</code> ⟨don’t worry about it⟩<code>)</code>
</table>
<p>I won’t dwell on the first six rows (<code>core:wildcard</code>, <code>core:var</code>, <code>core:quote</code>, <code>core:and</code>, <code>core:or</code>, <code>core:not</code>) because they’re not very interesting. <code>Core:wildcard</code> is spelled <code>_</code> in the high-level pattern language, <code>core:var</code> is just written as a variable name; <code>core:and</code> and <code>core:or</code> are now restricted to two subpatterns each. Otherwise, you can pretty much find everything you need to know about these from the documentation of the high-level forms in <a href="https://srfi.schemers.org/srfi-262/srfi-262.html">SRFI 262</a>.</p>
<p><code>Core:row</code> is where things start to get slightly more interesting. This pattern isn’t directly exposed to high-level forms, but it’s used for the patterns which can come after the procedure expression in a high-level <a href="https://srfi.schemers.org/srfi-262/srfi-262.html#entry:interro"><code>?</code> pattern</a> and the value patterns of a <a href="https://srfi.schemers.org/srfi-262/srfi-262.html#entry:darrow"><code>=&gt;</code> pattern</a>. They’re also used for each of the individual values at the top-level of a multiple-pattern <code>%core-match-lambda</code> form.</p>
<p>In a row pattern, all the subpatterns have to match, but the compiler is allowed to test them in any order. While a full rationale will have to wait until the next episode, in general the order in which it makes most sense to test subpatterns is not always the left-to-right order in which they appear in the source code. In the <code>(cons x '())</code> example, we’re far more interested in the cdr of this pair than we are in the car, because the cdr can tell us whether the pattern matches or not; only once we know that the cdr is the empty list should we bother trying to load the car.</p>
<p><code>Core:apply</code> corresponds directly to the high-level <code>=&gt;</code> pattern (which was called <code>apply</code> in earlier drafts of the SRFI) but has a different structure, to do with the need to give each value its own subject variable. The ⟨expression⟩ gives the procedure which is to be applied to the pattern’s subject; the ⟨identifier⟩s are subject variables to which each respective returned value will be bound; finally, the ⟨core pattern⟩ will be matched in an environment which includes those subject variables.</p>
<p><code>Core:subject</code> is the last piece of the puzzle; it simply says which subject variable names the subject of its own subpattern.</p>
<p>How this all fits together is probably best illustrated by example. Our <code>(cons x '())</code> example expands, within the high-level pattern matching language, to this:</p>
<pre><code>(? pair?
   (=&gt; car x)
   (=&gt; cdr &#39;()))</code></pre>
<p>which in core patterns expands to this:</p>
<pre><code>(core:and (core:? pair?)
          (core:row
           (core:apply car (g1) (core:row (core:subject g1 (core:var x))))
           (core:apply cdr (g2) (core:row (core:subject g2 (core:quote ()))))))</code></pre>
<p>A single-subpattern <code>core:row</code> isn’t very useful, but an equivalent expansion using <a href="https://srfi.schemers.org/srfi-1/srfi-1.html#car%2Bcdr">SRFI 1’s <code>car+cdr</code></a><a href="#fn2" class="footnote-ref" id="fnref2" role="doc-noteref"><sup>2</sup></a> would be:</p>
<pre><code>(? pair?
   (=&gt; car+cdr x &#39;()))</code></pre>
<p>which in core patterns is:</p>
<pre><code>(core:and (core:? pair?)
          (core:row
           (core:apply car+cdr (g1 g2)
            (core:row
             (core:subject g1 (core:var x))
             (core:subject g2 (core:quote ()))))))</code></pre>
<p>Something to note here is what <code>core:var</code> actually, formally does: it takes its subject variable and turns it into a pattern variable. <code>g1</code> is a hidden, generated variable; <code>x</code> is bound within its clause to the exact same value.</p>
<p>That’s more or less it for the core pattern language; I hope it’s more-or-less clear how it works. I’ve omitted <code>core:seq</code> from this description. If this series ever covers <code>core:seq</code> and the implementation of the pattern matcher’s functionality for running simple regular expressions over sequences of Scheme values, it will be in a bonus episode, possibly quite a bit later. I’m leaving it out here because the implementation is still an unhappy mess, much more so than any other part; also, <a href="https://srfi-email.schemers.org/srfi-262/msg/34134164/">Wolfgang Corcoran-Mathe has pointed out</a> that the current API mixes idioms in a confusing way, and fixing that might involve some other deep changes to how it’s implemented.</p>
<h2 id="the-abstract-syntax-tree-ast">The Abstract Syntax Tree (AST)</h2>
<p>As covered in <a href="/posts/2025-09-29-extensible-match-decision-tree-story.html">a previous missive</a>, core patterns are pseudo-record syntax objects; accessing fields from such syntax objects is slow; real records are much faster; so to make decision tree generation go faster, there’s an additional layer, called the abstract syntax tree, implemented as real Scheme records. This is in <a href="https://codeberg.org/dpk/extensible-match/src/commit/924ebeb58653ab8b77a51b45c4de29fb32f88dee/extensible-match/ast.sls"><code>(extensible-match ast)</code></a>, along with the code to convert core patterns to the AST. (That’s invoked by the long chain of procedure calls that drives the whole compilation process in <code>%core-match-lambda</code>, but defined here.)</p>
<p>Actually, though, there’s (at least) one more reason besides speed that the AST is better to work with going forward – see if you can spot it in the definition:</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode scheme"><code class="sourceCode scheme"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a>(<span class="kw">define-record-type</span> primitive-pattern)</span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a>(<span class="kw">define-record-type</span> pattern</span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a>  (fields subject)</span>
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a>  (parent primitive-pattern))</span>
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a>(<span class="kw">define-record-type</span> wildcard-pattern</span>
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a>  (parent pattern))</span>
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a>(<span class="kw">define-record-type</span> var-pattern</span>
<span id="cb5-8"><a href="#cb5-8" aria-hidden="true" tabindex="-1"></a>  (fields name)</span>
<span id="cb5-9"><a href="#cb5-9" aria-hidden="true" tabindex="-1"></a>  (parent pattern))</span>
<span id="cb5-10"><a href="#cb5-10" aria-hidden="true" tabindex="-1"></a>(<span class="kw">define-record-type</span> quote-pattern</span>
<span id="cb5-11"><a href="#cb5-11" aria-hidden="true" tabindex="-1"></a>  (fields datum)</span>
<span id="cb5-12"><a href="#cb5-12" aria-hidden="true" tabindex="-1"></a>  (parent pattern))</span>
<span id="cb5-13"><a href="#cb5-13" aria-hidden="true" tabindex="-1"></a>(<span class="kw">define-record-type</span> and-pattern</span>
<span id="cb5-14"><a href="#cb5-14" aria-hidden="true" tabindex="-1"></a>  (fields subpat_1 subpat_2)</span>
<span id="cb5-15"><a href="#cb5-15" aria-hidden="true" tabindex="-1"></a>  (parent primitive-pattern))</span>
<span id="cb5-16"><a href="#cb5-16" aria-hidden="true" tabindex="-1"></a>(<span class="kw">define-record-type</span> or-pattern</span>
<span id="cb5-17"><a href="#cb5-17" aria-hidden="true" tabindex="-1"></a>  (fields subpat_1 subpat_2)</span>
<span id="cb5-18"><a href="#cb5-18" aria-hidden="true" tabindex="-1"></a>  (parent primitive-pattern))</span>
<span id="cb5-19"><a href="#cb5-19" aria-hidden="true" tabindex="-1"></a>(<span class="kw">define-record-type</span> row-pattern</span>
<span id="cb5-20"><a href="#cb5-20" aria-hidden="true" tabindex="-1"></a>  (fields subpats)</span>
<span id="cb5-21"><a href="#cb5-21" aria-hidden="true" tabindex="-1"></a>  (parent primitive-pattern))</span>
<span id="cb5-22"><a href="#cb5-22" aria-hidden="true" tabindex="-1"></a>(<span class="kw">define-record-type</span> not-pattern</span>
<span id="cb5-23"><a href="#cb5-23" aria-hidden="true" tabindex="-1"></a>  (fields subpat)</span>
<span id="cb5-24"><a href="#cb5-24" aria-hidden="true" tabindex="-1"></a>  (parent primitive-pattern))</span>
<span id="cb5-25"><a href="#cb5-25" aria-hidden="true" tabindex="-1"></a>(<span class="kw">define-record-type</span> ?-pattern</span>
<span id="cb5-26"><a href="#cb5-26" aria-hidden="true" tabindex="-1"></a>  (fields predicate-id predicate-expr)</span>
<span id="cb5-27"><a href="#cb5-27" aria-hidden="true" tabindex="-1"></a>  (parent pattern))</span>
<span id="cb5-28"><a href="#cb5-28" aria-hidden="true" tabindex="-1"></a>(<span class="kw">define-record-type</span> apply-pattern</span>
<span id="cb5-29"><a href="#cb5-29" aria-hidden="true" tabindex="-1"></a>  (fields procedure-id procedure-expr vars subpat)</span>
<span id="cb5-30"><a href="#cb5-30" aria-hidden="true" tabindex="-1"></a>  (parent pattern))</span></code></pre></div>
<p>I’ve omitted the AST equivalent of <code>core:seq</code> for simplicity.</p>
<p>There are a couple of improvements here. I was so determined to kill <code>free-identifier=?</code> from the decision tree generator entirely that the expressions naming the procedures for <code>?</code> and <code>=&gt;</code>/<code>apply</code> patterns are now interned, giving them each a unique integer for the decision tree generator to use as a proxy for whether they are the same identifier. (This was probably a premature optimization, in hindsight.)</p>
<p>But the big improvement is in the (not very well-named) distinction between a <code>pattern</code> and a <code>primitive-pattern</code>. Notice that <code>core:subject</code> is gone from the AST! In its place, every pattern that needs to know the name of its subject variable has it stored directly within it.</p>
<p>In the core pattern language, you don’t actually know what <code>(core:quote ())</code> means without looking for a lexically enclosing <code>core:subject</code>: in our <code>(cons x '())</code> example, <code>(core:quote ())</code> alone could be looking at the pair (no match), the car (not intended, but might incorrectly match), or the cdr (as intended).</p>
<p>Making this change made the decision tree generator much simpler, apart from the speed boost from using records instead of syntax objects.<a href="#fn3" class="footnote-ref" id="fnref3" role="doc-noteref"><sup>3</sup></a> It no longer has to track a lexically implicit subject for each pattern.</p>
<p>You might wonder why the code doesn’t do this already during pattern expansion, and why core patterns don’t include their subjects in the same way the AST does. Well, maybe ideally it would do this, but in the sense of having everything in its right place at the right time, it’s probably the right thing for simplicity. Doing it this way lets the expansion of patterns happen completely independently of any context; we add that context to each individual subpattern later.</p>
<p>Tracking just the state of each expansion in continuation-passing style is tricky enough; adding the current lexical subject variable as extra state to pass around would make it more complicated. Moreover, continuation-passing expansions imply that the expander’s final result has to be a syntax object anyway – it’s not safe to go straight from unexpanded patterns to AST records – so let’s take the opportunity afforded by the <em>need</em> to have a slightly redundant layer in order to at least make the process of creating that redundant layer easier. That said, if Scheme <em>does</em> one day grow a way to call a transformer and apply marks independently of the expander’s main loop, it will probably then be time to get rid of core patterns and track the current subject variable implicitly while expanding directly into AST records.<a href="#fn4" class="footnote-ref" id="fnref4" role="doc-noteref"><sup>4</sup></a></p>
<h2 id="the-other-half-of-the-clause">The other half of the clause</h2>
<p>We’re not quite done with the AST. We’ve talked a lot about the left-hand side of each pattern matching clause – the pattern – and we’ll continue to do so. But there is another side, which is what happens when the pattern of a clause is known to have matched.</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode scheme continue"><code class="sourceCode scheme"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a>(<span class="kw">define-record-type</span> action</span>
<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a>  (fields procedure args))</span>
<span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a>(<span class="ex">define</span><span class="fu"> failure-action </span>(make-action #&#39;fail &#39;()))</span>
<span id="cb6-4"><a href="#cb6-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-5"><a href="#cb6-5" aria-hidden="true" tabindex="-1"></a>(<span class="kw">define-record-type</span> patact</span>
<span id="cb6-6"><a href="#cb6-6" aria-hidden="true" tabindex="-1"></a>  (fields pattern action))</span></code></pre></div>
<p>There’s not too much to say here. When a top-level pattern matches, its corresponding action is called. It’s a procedure, named by a generated identifier for each clause, plus a list of the variables bound by the corresponding pattern (which become arguments of the procedure). If no pattern matches, we call a special action called <code>fail</code> which just raises an exception.</p>
<p>A pattern and an action together make a clause, but here we call it a patact.</p>
<h2 id="unifying-subject-variables">Unifying subject variables</h2>
<p>That’s all the <em>what</em>; let’s finish up with a quick look at another bit of <em>how</em>. We’ll talk a lot more about what it takes to generate optimal code for a pattern match next time, but there’s one step that takes place to get the patterns in a form that’s easier to optimize.</p>
<p>Recall that each (sub)pattern expansion happens independently, without knowledge of the context from other expansions; and also that the expansion of <code>=&gt;</code> patterns into <code>core:apply</code> patterns. What this means is that equivalent applications generate distinct subject variables; we really want equivalent applications to create equivalent subject variables!</p>
<p>To see how, let’s put the example we’ve used so far into context:</p>
<div class="sourceCode" id="cb7"><pre class="sourceCode scheme"><code class="sourceCode scheme"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a>(<span class="ex">define</span><span class="fu"> </span>(last ls) <span class="co">; returns the last element in a list</span></span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a>  (match ls</span>
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a>    ((<span class="kw">cons</span> x &#39;())  x)</span>
<span id="cb7-4"><a href="#cb7-4" aria-hidden="true" tabindex="-1"></a>    ((<span class="kw">cons</span> <span class="op">_</span> ls*)  (last ls*))))</span></code></pre></div>
<p>These two patterns will expand, respectively, into the same core pattern we saw above and another very similar one:</p>
<pre><code>(core:subject ls
 (core:and (core:? pair?)
           (core:row
            (core:apply car (g1) (core:row (core:subject g1 (core:var x))))
            (core:apply cdr (g2) (core:row (core:subject g2 (core:quote ())))))))
;; action: (g3 x)

(core:subject ls
 (core:and (core:? pair?)
           (core:row
            (core:apply car (g4) (core:row (core:subject g4 (core:wildcard))))
            (core:apply cdr (g5) (core:row (core:subject g5 (core:var ls*)))))))
;; action: (g6 ls*)</code></pre>
<p>We applied <code>car</code> and <code>cdr</code> to <code>ls</code> in each pattern, but the result was called <code>g1</code> and <code>g2</code> in the first pattern and <code>g4</code> and <code>g5</code> in the second one. We only want to call each procedure once – if at all – in the generated code, so it would be nice to only generate one variable. This is what the pattern subject unification pass does: for every application of the same procedure to the same subject, the created subject variables should have the same name. The result is that the first pattern will still look the same, but the second will be (the AST-level equivalent of) this:</p>
<pre><code>(core:subject ls
 (core:and (core:? pair?)
           (core:row
            (core:apply car (g1) (core:row (core:subject g1 (core:wildcard))))
            (core:apply cdr (g2) (core:row (core:subject g2 (core:var ls*)))))))
;; action: (g6 ls*)</code></pre>
<p>It does this in a recursive pass which looks for combinations of subject, procedure, and number of output values<a href="#fn5" class="footnote-ref" id="fnref5" role="doc-noteref"><sup>5</sup></a> it’s not seen before; when it finds one, it puts the variable names that were generated for that pattern’s version into a hash table. When it sees the same combination of subject, procedure, and co-arity again, it replaces that AST <code>apply</code> pattern with a version whose subject variables are renamed, and recursively traverses the <code>apply</code> pattern’s subpattern to change instances of those subject variables appearing within <code>quote-pattern</code>s, <code>var-pattern</code>s, other <code>apply-pattern</code>s etc. to the unified name.</p>
<p>This is also something that could be done during expansion, but it’s just simpler to do it as an extra pass after expansion because it makes expansion independent of external context.</p>
<h2 id="potential-future-ast-transformations">Potential future AST transformations</h2>
<p>Subject variable unification is the currently the only optimization pass that’s done on the AST of the patterns before handing over to the decision tree generator, which is responsible for most of the work of finding an efficient way to match a set of patterns. But there are a couple of other passes the pattern match compiler could make at this point to improve the quality of generated code, or make the decision tree generator simpler.</p>
<dl>
<dt>Rewrite complex <code>not</code> patterns</dt>
<dd>
<p>At the moment, using a <code>not</code> pattern will completely frustrate the optimizer. It’s not too bad if you only put <code>not</code> around a datum or a predicate – but if you put it around an (implicit) <code>and</code>, the optimizer will only know how to compile it and its entire subpattern at once, and be unable to avoid potentially repeating equivalent computations for multiple similar patterns. Being able to mix in patterns which do match when they don’t match anywhere arbitrarily would just add too much complexity to the decision tree generator.</p>
<p>Fortunately, in 1846, compiler engineer Augustus De Morgan <a href="https://en.wikipedia.org/wiki/De_Morgan%27s_laws">formalized an intuition</a> about the relationship between negations, conjunctions, and disjunctions, which could eventually be applied here to ensure that the <code>not</code> pattern never appears outside of an <code>and</code> or <code>or</code> pattern. Ensuring it also never appears outside of <code>row</code> would be ideal, but the obvious solution – turning e.g. <code>(not (row pat1 pat2))</code> into <code>(or (not pat1) (not pat2))</code> – may be an accidental pessimization because <code>or</code> has an order-of-evaluation restriction which <code>row</code> doesn’t.</p>
<p>This AST transformation alone would be an improvement for the decision tree generator, but the real improvement would come with recognizing common <code>not</code> operations and being able to fully optimize them. Another benefit of this transformation would be the ability, in theory, to (sometimes) issue compile-time warnings about patterns like <code>(not _)</code> which will never match.</p>
</dd>
<dt>Remove <code>quote</code> patterns from the AST</dt>
<dd>
<p>A <code>(core:quote val)</code> pattern can be rewritten as <code>(core:? (lambda (x) (equal? x 'val)))</code>, but I don’t do this during expansion because it makes it harder to recognize that two <code>quote</code> patterns are equivalent. Since conversion from core patterns to the AST includes the recognition of equivalent expressions in order to give them unique integer IDs, it could generate an appropriate <code>lambda</code> expression for each mentioned datum then.</p>
<p>This would be good to do together with the previous one, because adding support for explicitly recognizing <code>not</code> patterns in the decision tree generator would add complexity there; removing <code>quote</code> patterns from the AST would remove some related complexity to balance that out. The cp0-equivalent pass of the Scheme compiler will then get rid of the <code>lambda</code> expression again.</p>
</dd>
<dt>Re-chain <code>and</code> patterns to only be nested on the right-hand side</dt>
<dd>
<p>Another way to frustrate the optimizer at the moment is to write something like <code>(and (and a b) c)</code> instead of <code>(and a (and b c))</code>. The optimizer will look for things it can do in the left-hand side of an <code>and</code>, but if it sees another <code>and</code> it isn’t smart enough to be able to look again at <em>that</em> <code>and</code>’s left-hand side, mostly because reconstructing the leftwardly-nested <code>and</code> after deciding to take action on its contents would be too much of a pain. The ordering relationship for these two nesting structures is the same, so an AST transformation should rewrite them all into a form that’s friendlier for the decision tree generator.</p>
<p><code>Or</code> patterns, on the other hand, don’t have this problem, because the decision tree generator will flatten those out into entirely separate patterns while it’s working, and that process can handle any nesting structure it finds.</p>
</dd>
</dl>
<h2 id="next-time">Next time</h2>
<p>The decision tree generator! This will probably be the third of a four-part series, before the final part – a short coda on turning decision trees into executable code.</p>
<hr />
<p><i>Updated 26 November 2025 to reflect some improvements in the structure of the AST, and also to actually add links to the other libraries mentioned here.</i></p>
<hr />
<p><i>The next part of this series is online <a href="https://crumbles.blog/posts/2025-11-28-extensible-match-decision-tree.html">here</a>.</i></p>
<section id="footnotes" class="footnotes footnotes-end-of-document" role="doc-endnotes">
<hr />
<ol>
<li id="fn1"><p>I would try to promise that, like Knuth, I will have the decorum to not continue the digestive metaphor beyond the stomach, but I fear I would be promising too much.<a href="#fnref1" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn2"><p>Which is the <em>real</em> procedural codata counterpart to the data <code>cons</code>.<a href="#fnref2" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn3"><p>The old, slow version of the decision tree generator had a <a href="https://codeberg.org/dpk/extensible-match/src/commit/7bf87bf1e485a628773f58d358334b66e071f16b/extensible-match/decision-tree.sls#L468">pretty awful procedure whose job was to push <code>core:subject</code> patterns down the core pattern tree lazily</a>.<a href="#fnref3" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn4"><p>Sheesh, that one missing expander feature alone would probably let me get rid of about a quarter of the lines of implementation in extensible-match.<a href="#fnref4" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn5"><p>For very sad reasons <a href="https://srfi.schemers.org/srfi-262/srfi-262.html#design-notes:transformation-patterns-mv">explained in the SRFI</a>, you can’t use a <code>=&gt;</code> pattern to match on the number of values a procedure returns. It’s nonetheless possible to construct valid, non-erroring patterns where the same procedure applied to the same subject would return different numbers of values and match different numbers of patterns. Writing one is an exercise for the reader.<a href="#fnref5" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
</ol>
</section>]]></summary>
</entry>
<entry>
    <title>Tour of a pattern matcher: expression and pattern expansion</title>
    <link href="https://crumbles.blog/posts/2025-11-09-extensible-match-front-end.html" />
    <id>https://crumbles.blog/posts/2025-11-09-extensible-match-front-end.html</id>
    <published>2025-11-09T18:18:44Z</published>
    <updated>2025-11-09T18:18:44Z</updated>
    <summary type="html"><![CDATA[<p>I’ve posted a lot on Mastodon (and <a href="/posts/2025-09-29-extensible-match-decision-tree-story.html">a little bit here</a>) about the adventures I’ve had implementing <a href="https://codeberg.org/dpk/extensible-match">an extensible pattern matcher in Scheme</a>. Well, <a href="https://srfi.schemers.org/srfi-262/srfi-262.html">the SRFI</a> isn’t too far off finalization now, and the implementation is unlikely to change dramatically, so I reckoned it might be worthwhile to take readers on a tour of how the implementation works: for one thing, for those who want to be able to dive in and understand the code in future and maybe hack on it; for another, as a further propaganda piece in favour of a style of programming still under-used in Scheme. But also I think there’s a lot the budding Schemer can learn about Scheme, and a lot the budding functional programmer can learn about this central construct of functional languages, from studying the implementation. There’s still a lot of mess in the code that I’m moderately embarassed about – we’ll deal with one such embarassment today, albeit a less immediately avoidable one. But I hope that will get cleaned up over time; like visiting a newly-furnished building where they haven’t yet discovered that putting that cabinet <em>there</em> will make the door handle hit the wall and damage the wallpaper, you can see the idea of the place, and those little things will get sorted eventually.</p>
<p>As mentioned last time I talked about the implementation, extensible-match is a Scheme macro complex enough that it can be seen as a compiler unto itself. There’s a sense in which this is a truism – every Scheme macro is a compiler from a dialect of Scheme containing that macro’s syntactic extension into a dialect that doesn’t use it, at least locally – and attempting to define what makes a macro a compiler beyond that is somewhat impressionistic. But extensible-match has intermediate representations, multiple passes, an optimizer, and a code generator to turn all of that back into Scheme code: it’s definitely on the side of a compiler, taken as a whole. But today, following the path that your patterns actually take when the compiler digests them and turns them into running Scheme code, we’ll just take a look at the front end, which looks more like a traditional Scheme macro definition. Even here, there are a few things worth pointing out along the way!</p>
<h2 id="the-match-forms">The match forms</h2>
<p>We start our journey in the uncreatively named <a href="https://codeberg.org/dpk/extensible-match/src/branch/master/extensible-match/match.sls"><code>(extensible-match match)</code></a> library, which provides all the Scheme expression forms with which pattern match programmers actually use to interact with the pattern matcher. There are <a href="https://srfi.schemers.org/srfi-262/srfi-262.html#spec:scheme-pattern-matching-syntax">a dozen distinct macros</a> for getting access to the pattern matcher – from the very basic <code>match</code> itself (which will probably consistute about 90% of real-world uses) to the somewhat obscure <code>match-letrec*</code>.</p>
<p>Under the hood, though, all of them ultimately expand into a use of <code>match-lambda</code>. Partly this is for <a href="https://nhplace.com/kent/PS/Lambda.html">party</a> reasons – <a href="https://dspace.mit.edu/handle/1721.1/5600">one thing to name them all, one thing to define them</a> and so on – but also <code>match-lambda</code> is, in fact, the fundamental pattern matching form, in that it offers <em>almost</em> everything that all of the subordinate pattern matching forms need. Specifically, unlike <code>match</code> it handles multiple values without consing them up into a list first; unlike <code>match-let</code> and friends it has multiple clauses which it tries in succession.</p>
<p><code>Match-lambda</code> in turn is implemented in terms of <code>case-lambda</code>, the Scheme way of making a procedure which takes varying number of arguments by simply dispatching to different code based on the number of them. <code>Case-lambda</code> is built in to Scheme, so the first bit of pattern matching – discerning what to do based on the number of values given – is done for us. All <code>match-lambda</code> does is group the pattern matching clauses (which of course can discern more finely) by the number of values they expect, creates a <code>case-lambda</code> clause for each of those groups, and puts a <code>%match-lambda</code> inside each of those clauses. This <code>%match-lambda</code> handles the next stage: it still handles multiple clauses, but each of them expect the same number of values.</p>
<p>This is a good moment to take a short diversion into macro efficiency, or rather into deliberate inattention to efficiency. An ideal macro expansion will look different depending on the exact forms it receives. In this case, you might think it would somehow optimize the macro if it treated the (common!) case where every <code>match-lambda</code> clause takes the same number of values specially, and expanded directly into a single <code>lambda</code> instead of a <code>case-lambda</code> with only one clause, which itself is going to turn into a call to a <code>lambda</code> expression once this <code>%match-lambda</code> is expanded. Not so!</p>
<p>When writing Scheme macros, it’s always better to just handle the general case and let the compiler itself deal with the redundant code you might generate in special cases. This is the essence of the <a href="https://www.youtube.com/watch?v=LIEX3tUliHw">Guaranteed-Optimization Clause of the Macro-Writer’s Bill of Rights</a>. A <code>case-lambda</code> with only one clause is usually treated by the expander as identical to a <code>lambda</code> even before any optimization begins; resulting structures like <code>(lambda (x y z) ((lambda (x* y* z*) ...) x y z))</code> are trivial for the compiler itself to remove and turn into a more sane form. Adding it into the macro implementation directly would add more lines of code, but would bring no benefit at all the moment the expansion got out of the expander: in a Scheme compiler, getting rid of pointless make-work code like this is usually the first pass done after macro expansion, a combined inlining/partial evaluation/constant folding pass which Chez Scheme calls ‘cp0’ and Guile calls ‘peval’.<a href="#fn1" class="footnote-ref" id="fnref1" role="doc-noteref"><sup>1</sup></a> Compiler optimizations which, in a language without macros, seem fairly pointless – ‘who’d ever write code like <em>that</em>?’ – become valuable with macros, and become more valuable the more powerful the macro system is – ‘oh, code generated by other code looks like that!’</p>
<p>This is the point of the ‘guaranteed-optimization clause’: it lets us be laudably lazy as macro authors by guaranteeing us that our code will still run fast even if we don’t take the time to deal with redundant expressions; and it lets the users of macros know that they don’t have to worry about the performance impact of using a higher-level syntactic construct. Macros generate all sorts of silly code structures, not only because their authors don’t bother to deal with special cases, but also because there’s a lot that macros don’t know about the context they’re being used in. But that information which is trivially accessible for the Scheme optimizer once it has a fully-expanded abstract syntax tree. An example of not needing to worry about special cases is that, even in performance-sensitive code, if your macro takes in an expression but only wants to evaluate it once, you might be tempted to try to recognize if the expression is a single identifier to avoid redundantly generating a <code>let</code> expression giving it a name local to your macro. But you don’t need to: the Scheme compiler can just take care of it! An example which isn’t just laudable laziness is when the expansion of a macro includes code which treats its input specially if it’s a certain type – but the Scheme compiler with the fully-expanded program at hand can run a type propagation pass and change the code to reflect its knowledge of the possible types a variable might have at the specific point of each individual use of the macro.</p>
<p>We’ll see a few more examples of this philosophy of ‘just let the Scheme compiler deal with it later’ throughout our tour. In general: ask not what you can do for your optimizer, ask what your optimizer can do for you! The <code>expand/optimize</code> procedure in Chez and Loko, or the <code>,optimize</code> REPL command in Guile, is your friend whenever you’re trying to work out what special cases it’s worth writing extra code to deal with vs. which would be wasted effort on something the compiler itself could do better anyway.</p>
<p>Anyway, <code>%match-lambda</code> turns out not to be the end of the chain of delegating to yet more primitive forms called <code>lambda</code>, although this final one doesn’t generate a redundant expression in the final code. This is an <em>extensible</em> pattern matching library, so patterns can be forms that the matcher itself doesn’t natively know how to interpret. In order to deal with those forms, we have to expand them into forms which it can natively deal with. That’s right, it’s a macro expander within a macro! (Kind of!)</p>
<h2 id="pattern-expansion">Pattern expansion</h2>
<p>So we move on to the <a href="https://codeberg.org/dpk/extensible-match/src/branch/master/extensible-match/expand.sls"><code>(extensible-match expand)</code></a> library, which contains the <code>expand-pattern</code> form which <code>%match-lambda</code> delegates to. The job of this library is to turn unexpanded patterns, containing all the weird and wonderful pattern syntax that users defined themselves, into ‘core patterns’ that the rest of the pattern match compiler can deal with directly. We’ll deal with the structure and rationale of core patterns in the next installment; for now, if you’re familiar with the structure of SRFI 262 patterns, they should look pretty familiar with an extra <code>core:</code> at the beginning of their name. For disambiguation, variables are now spelled <code>(core:var name-of-the-var)</code> and <code>_</code> is spelled <code>(core:wildcard)</code>, but they’re generally pretty similar in structure.</p>
<p>The interpretation of <em>non</em>-core patterns is actually done by yet another library, <a href="https://codeberg.org/dpk/extensible-match/src/branch/master/extensible-match/pattern-syntax.sls"><code>(extensible-match pattern-syntax)</code></a>, but this was a fairly late change which I made in order to be able to write an implementation-specific version of <em>only</em> that functionality for Guile, which doesn’t yet support identifier properties. Potentially in future, implementation-specific versions could appear for other Schemes which provide enough access to their internals to allow correct (or nearly correct) pattern syntax semantics to be impemented in some way other than actually using <a href="https://srfi.schemers.org/srfi-213/srfi-213.html">SRFI 213</a>. Anyway, the two libraries still work together pretty much as one.</p>
<p>The forms in these two libraries are, surprisingly, themselves macros and not procedures, and they’re used in a bit of an unusual way. The problem is applying macro hygiene to the pattern syntax expansions. Put (very) roughly, macro hygiene works by the expander creating a new, unique token every time one expansion of one macro use takes place; when the user-written code which writes the expansion has returned, the expander goes in and changes the names of all the identifiers in the returned code by adding that magic token.</p>
<p>Well, <code>extensible-match</code> itself is a macro, and doesn’t have a way to create one of those magic tokens to apply to identifiers. We have to let the macro expander of the Scheme implementation itself do it for us – but there’s no standard Scheme API for that. What do?</p>
<p>The answer is <a href="https://www.schemeworkshop.org/2000/#:~:text=Writing%20macros%20in%20continuation-passing%20style">macros in continuation-passing style</a>. Hilsdale and Friedman’s paper, linked, is a little obscure and doesn’t really make a compelling case for using this in practice apart from hinting at the possibility of some really dirty tricks (<a href="https://okmij.org/ftp//Scheme/Dirty-Macros.pdf">Petrofsky extraction, Kiselyov defilement, etc.</a>). It’s a well-known trick, though, for implementing Scheme macros which can expand to a language other than Scheme (embedded within Scheme). Patterns are an example of such a language; <a href="https://github.com/ashinn/chibi-scheme/blob/master/lib/chibi/loop/loop.scm">Alex Shinn’s loop macro clauses are another</a> (Taylor R. Campbell’s version of this looping macro is more popular, but I’ve linked Shinn’s original implementation as it’s shorter and shows the continuation-passing trick more clearly).</p>
<p>The idea is that a continuation-passing macro matches code of the form</p>
<pre><code>(the-keyword input-of-the-macro ... next-keyword . next-arguments)</code></pre>
<p>and always transforms it into the form</p>
<pre><code>(next-keyword output-of-the-macro . next-arguments)</code></pre>
<p>So the macro receives the name of another macro which will further process its own output. Because the whole output after this transformation goes back into the expander, which thinks a complete expansion is finished, it will apply a new magic token to everything in the <code>output-of-the-macro</code>. If <code>output-of-the-macro</code> is itself a macro use in the sublanguage being expanded into, <code>next-keyword</code> can again recursively transform it into a continuation-passing style macro use at the Scheme level, referring back to itself as the continuation.</p>
<p>You can do this entirely in <code>syntax-rules</code>! Shinn’s <code>loop</code> does exactly this – macro-extensible macros don’t have to involve the most advanced expander features available. But offering direct access to an extension API based on continuation-passing macros has some disadvantages:</p>
<ol type="1">
<li><p>It exposes implementation details of the sublanguage to writers of extensions to the sublanguage, failing to maintain an appropriately opaque abstraction barrier between usage and implementation. One can imagine that someone might come along and decide to actually destructure the <code>next-arguments</code>, thinking that they can understand what the sublanguage implementation is packing in there and re-use that information for a nifty feature. In so doing they create a <a href="https://www.hyrumslaw.com/">Hyrumesque nightmare</a> for the developer of the sublanguage, who will break their nifty feature the next time they decide that this internal continuation structure needs changing.</p>
<p>The problem is only exacerbated by continuation-passing macro extension APIs which provide more than one continuation, or more information, to their writers. <a href="https://www.khoury.northeastern.edu/home/shivers/papers/loop.pdf">Olin Shivers’s loop macro</a> and <a href="https://mumble.net/~campbell/scheme/foof-loop.txt">Taylor R. Campbell’s implementation of foof-loop</a> both do this: the result may be powerful, but from the point of the designer of the sublanguage, it is inflexible, hemming in future extensions of the power of the sublanguage itself.</p></li>
<li><p>It fails to establish a distinct namespace for extension keywords to safely live in, outside of which uses of those keywords are either errors (as in <code>(chibi loop)</code>) or have distinct but related semantics (as in the pattern matcher). It also fails to prevent unrelated keywords being accidentally, mistakenly used as keywords within the sublanguage, which can expand into unrelated forms with unexpected behaviour.</p></li>
<li><p>It makes error reporting for incorrect uses of extensions to the sublanguage confusing, since upon failure to match any <code>syntax-rules</code> clause, it has no idea which parts of the macro are implementation details of the sublanguage and which are parts written <em>in</em> the sublanguage and directly entered by the programmer.</p></li>
<li><p>Macro writers have to be careful to always return to the continuation they were given. (On the other hand, providing <code>quote</code> or <code>quote-syntax</code> as the continuation keyword makes it easy to debug more complex expansions by single-stepping; whereas most Scheme implementations unfortunately do not provide <a href="http://clhs.lisp.se/Body/f_mexp_.htm"><code>macroexpand-1</code></a> for forms in the Scheme language itself.)</p></li>
</ol>
<p>The extensible pattern matcher side-steps all of this by capturing the transformer procedures of its extension macros directly (in identifier properties) and calling them only on the user input, then embedding the output of the transformer procedure into a continuation-passing macro. The only information the transformer for pattern syntax has access to is the syntax object representing the pattern use itself; if there’s an error, it’ll be expressed in terms of this only and not in terms of some low-level form the pattern syntax user isn’t interested it. All the pattern syntax transformer has to do is return its expansion, and what the pattern expander does with it after that is none of its business: a clean abstraction barrier, just like real Scheme macros.</p>
<p>You might ask why, having thus eliminated the usual reasons for writing continuation-passing macros, the pattern syntax expander isn’t just a simple procedural recursion like this:</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode scheme"><code class="sourceCode scheme"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a>(<span class="ex">define</span><span class="fu"> </span>(expand-pattern-syntax stx)</span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a>  (<span class="kw">syntax-case</span> stx (core-pattern)</span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a>    <span class="co">;; Base case, a fully expanded pattern identified by some special head:</span></span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a>    ((core-pattern expansion) #&#39;expansion)</span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a>    <span class="co">;; Recursive case of a pattern with a pattern syntax keyword:</span></span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a>    ((keyword <span class="op">.</span> <span class="op">_</span>)</span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a>     (expand-pattern-syntax ((look-up-pattern-transformer #&#39;keyword) stx)))))</span></code></pre></div>
<p>Indeed, <a href="https://arxiv.org/pdf/1106.2578">Sam Tobin-Hochstadt’s paper on extensible pattern matching in Racket</a> presents it as if it did work this way.</p>
<p>As mentioned above, the reason is hygiene: we want to go back into the Scheme expander after each step and apply a unique magic token to any inserted identifiers for each individual use of pattern syntax, so that the names don’t collide with one another. This is, admittedly, somewhat pedantic: there’s no real point to introducing identifiers with the current structure of patterns, but with <a href="https://srfi.schemers.org/srfi-262/srfi-262.html#design-notes:future-extensions">future extensions</a> there might be, and it’s possible that some unusual pattern macro implementation might have a reason to do it anyway – so it’s worth taking the effort to do the right thing.</p>
<p>All of <code>(extensible-match expand)</code> is thus concerned with implementing a recursive expansion of pattern syntax while maintaining this hygienic property, which it has to do in continuation-passing style. For example, expanding a <code>core:and</code> or <code>core:or</code> means each of their sides have to be expanded recursively, then those expanded forms placed back in a new <code>core:and</code> or <code>core:or</code> which is then deemed fully expanded. If the <code>core:and</code> or <code>core:or</code> is actually a subpattern of some larger pattern – another <code>core:and</code> or <code>core:or</code> even (the core versions of these forms can only be two-armed, as a simplification) – they then have to pass this fully-expanded form back to the ongoing expansion of that larger pattern, so that it in turn can continue and reconstruct its input.</p>
<p>Tobin-Hochstadt’s presentation in his paper is actually something of a pedagogical lie, although it’s not much of one since Racket <em>does</em> have a way to apply its expander’s magic tokens without going all the way back into the expander, and Racket’s pattern matcher uses that to avoid this continuation-passing macro malarkey. This is therefore all a bit embarassing for a Schemer who can look across the aisle and see that Racket has always had better ways of doing this; <a href="https://codeberg.org/dpk/presrfis/src/branch/master/call-transformer.md">hopefully, one day, Scheme will too, and all this continuation-passing nonsense will be gone</a>.</p>
<p>As one last point, we don’t expand only one pattern at once – in <code>match-lambda</code> we have a number of clauses, each of which has a number of patterns (for each of the values handled by that clause). So we have not only an <code>expand-pattern</code> in continuation-passing style, but an <code>expand-patterns</code> too, which expands all the patterns in a list of patterns and passes them all on as a list of core patterns; then an <code>expand-patternses</code> which expands all the patterns in a list of list of patterns, passing them on as a list of lists of core patterns, which is what <code>%core-match-lambda</code> actually uses directly. These, of course, work by invoking <code>expand-pattern</code> with a continuation which keeps track of the already-expanded patterns and the ones yet to be expanded. Phew!</p>
<p>All of that gets bundled up and, as <code>%match-lambda</code> requested, the final continuation of the expansion process is <code>%core-match-lambda</code>, the form which co-ordinates the whole pipeline of the <em>actual</em> compilation of patterns into Scheme code.<a href="#fn2" class="footnote-ref" id="fnref2" role="doc-noteref"><sup>2</sup></a> We’ll start talking about the stages <code>%core-match-lambda</code> puts the patterns through next time – from here on in, it’s all procedures, and no more expanding match macros into yet more macros!</p>
<h2 id="one-last-thing-that-almost">One last thing: that ‘<a href="#:~:text=it%20offers%20almost%20everything"><em>almost</em></a>’</h2>
<p>There’s one wrinkle in using <code>match-lambda</code> for everything: the forms which do recursive binding – <code>match-letrec</code>, <code>match-letrec*</code>, and <code>match-define</code> – practically speaking need access to something that <code>match-lambda</code> doesn’t give them: they need to know the names of all the variables bound by the pattern match. In order to know that, they have to expand the patterns and extract the list of variables before <code>match-lambda</code> does.</p>
<p>Racket actually expands the patterns twice for this functionality, but I think this is <a href="https://www.folklore.org/Were_Not_Hackers!.html">unnecessarily nondeterministic</a>. There is no guarantee that any syntax transformation has to be a pure function; while every expansion of a macro should have the same effect, if you throw in generated identifiers and the possibility of seeded hash functions used in a macro implementation and similar such things, there’s no guarantee that every expansion will be detectably <em>identical</em>. So my view is that it’s better to expand the patterns only once. <code>Match-letrec</code> and <code>match-define</code> therefore invoke <code>expand-patterns</code> or <code>expand-pattern</code> themselves, skip around the publically-exposed <code>match-lambda</code>, and go directly to <code>%core-match-lambda</code>. <code>Match-letrec*</code> is implemented in terms of <code>match-define</code>, letting the Scheme implementation take over the implementation of <code>letrec*</code> semantics.</p>
<h2 id="next-time">Next time</h2>
<p>I’m not sure exactly how much it will make sense to cover in each individual entry in this series, and thus also not sure how many entries there’ll be. But next time we’ll certainly look at the structure of core patterns and the abstract syntax tree, and maybe start to look at the real meat in the decision tree generator.</p>
<hr />
<p><i>Greetings, reader from the future! You can read the next part <a href="https://crumbles.blog/posts/2025-11-15-extensible-match-ast.html">here</a>.</i></p>
<section id="footnotes" class="footnotes footnotes-end-of-document" role="doc-endnotes">
<hr />
<ol>
<li id="fn1"><p>Most compiler courses: ‘Here is 90% on parsing and a tiny bit on register allocation; good luck and have fun!’</p>
<p>Kent Dybvig: calls the pass of his compiler which comes right <em>after</em> parsing and macro expansion ‘Compiler Pass Zero’, because parsing and expansion aren’t really compiling.<a href="#fnref1" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn2"><p>That’s right – just like Chez Scheme, the architecture of extensible-match is such that it doesn’t really consider expansion per se to be compilation.<a href="#fnref2" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
</ol>
</section>]]></summary>
</entry>
<entry>
    <title>Scheme Reports at Fifty: Where do we go from here?</title>
    <link href="https://crumbles.blog/posts/2025-10-18-scheme-reports-at-fifty.html" />
    <id>https://crumbles.blog/posts/2025-10-18-scheme-reports-at-fifty.html</id>
    <published>2025-10-18T16:43:53Z</published>
    <updated>2025-10-18T16:43:53Z</updated>
    <summary type="html"><![CDATA[<p><img src="/images/scheme-at-fifty.png" alt=""></p>
<p><i>Based on my talk at the Scheme Workshop 2025. You might prefer to have me <a href="https://www.youtube.com/live/Zl-M3avRPXI?si=hb651QDhylQBlakt&t=25780">talk it at you instead</a>.</i></p>
<p><i>We are holding an election to the Scheme Steering Committee. <a href="https://r7rs.org/sc/">Register to vote and nominate candidates!</a></i></p>
<hr />
<p>In December this year, the Scheme reports will turn fifty. As chair of the <a href="https://codeberg.org/scheme/r7rs/wiki/WG2-Members">working group</a> entrusted with <a href="https://r7rs.org/">the next major revision</a> of the report, I want to start a discussion in the Scheme community about what a good Scheme report looks like in 2025, and how it’s different from what it looked like in 1975, and from other times when the Scheme report was revised.</p>
<h2 id="who-is-the-scheme-report-for">Who is the Scheme report for?</h2>
<p>When making any document, we have to consider who we’re writing for, how they’re going to use it, and what they want and need from it in order to do their thing. For the Scheme report, the obvious two groups are users and implementers. These are very vague groupings, and like any attempt to divide up a group as large and diverse as the Scheme user base, one can’t at all pretend like every member of these groupings has the same views on every issue. Nonetheless, a rule of thumb is that users want their pet features added, creating pressure to grow the report;<a href="#fn1" class="footnote-ref" id="fnref1" role="doc-noteref"><sup>1</sup></a> implementers want to be able to actually create a compiler, creating pressure to keep the report smaller and simpler. But there are also clear subdivisions where even this clear breakdown falls apart, like in the embedded space, where both users and implementers want something that can work on limited-resource environments.</p>
<p>Another tension still rarely recognized is between users in education – students learning the language – and long-time Schemers who know it very well. The <cite>How to Design Programs</cite> approach seems to have worked for many more beginning students than <cite>Structure and Interpretation of Computer Programs</cite> ever did, because <cite>HtDP</cite> acknowledges from the start that assuming brand new students will be able to learn with a tool designed for people who are already experts means that tool can’t be as helpful to those just finding their feet in a new way of thinking.</p>
<p>But in Scheme we often get into philosophical and even political debates when we talk about what the Scheme report should be like; debates which have much more to do with an ideological belief about the nature of the language than with the direct, practical needs of the debaters.<a href="#fn2" class="footnote-ref" id="fnref2" role="doc-noteref"><sup>2</sup></a></p>
<h2 id="big-and-little-languages">Big and little languages</h2>
<p>A classic such argument is over whether certain versions of Scheme – usually R6RS, and sometimes also the one that my working group is producing – are ‘too big’. On the other hand, Scheme reports that don’t have this quality can be seen as being too ‘little’ for doing real programming work in.</p>
<p>Earlier, littler Scheme reports are often described by the makers of these arguments as a ‘diamond-like jewel’, or similar. The ‘diamond-like jewel’ wording is, I think, from the infamous ‘Worse is Better’ section of Richard P. Gabriel’s paper <a href="https://www.dreamsongs.com/Files/LispGoodNewsBadNews.pdf">‘Lisp: Good News, Bad News, and How to Win Big’</a>.</p>
<p>In 1998 Guy L. Steele, co-inventor of Scheme, read a keynote paper at OOPSLA entitled <a href="https://www.youtube.com/watch?v=lw6TaiXzHAE">‘Growing a Language’</a> (<a href="https://langev.com/pdf/steele99growing.pdf">transcript</a>). In the paper, he talks a lot about the tension between the qualities of a big language vs a little language, and in particular talks about Scheme, saying:</p>
<blockquote>
<p>Could not “The Right Thing” be a small language, not hard to port but with no warts?</p>
<p>I guess it could be done, but I, for one, am not that smart (or have not had that much luck). But in fact I think it can not be done. Five-and-twenty years ago, when users did not want that much from a programming language, one could try. Scheme was my best shot at it.</p>
<p>But users will not now with glad cries glom on to a language that gives them no more than what Scheme or Pascal gave them. They need to paint bits, lines, and boxes on the screen in hues bright and wild; they need to talk to printers and servers through the net; they need to load code on the fly; they need their programs to work with other code they don’t trust; they need to run code in many threads and on many machines; they need to deal with text and sayings in all the world’s languages. A small programming language just won’t cut it.</p>
</blockquote>
<p>From the things he names as examples of what users want to do with languages, it’s clear that Steele’s mind was very much on Java, and Java in 1998 at that. Some of these things just aren’t in fashion any more: some were thought to be good ideas at the time, but now seen as bad ideas; some of them just never caught on. But some of them are just as important as ever.</p>
<p>Of the things that are just as important as ever, though, many of those no longer seem like things a language should have to take care of; they are, rather, the province of third-party libraries.</p>
<p>That, in fact, is how Steele concludes ‘Growing a Language’: users should be able to grow the language itself by adding libraries.</p>
<p>But, it seems, the base of Scheme as it was in 1975, and even in 1998, was not enough. Most people today would agree that a programming language without even a standard error-handling mechanism is quite deficient, even if it does give one the ability to build one for oneself.</p>
<p>And, in fact, most Schemers agree that the best way for Scheme to go forward is to find better ways for Scheme users to grow the language. Our disagreement, then, is not about ‘bigness’ per se, but where the bigness comes from. Few would swallow a thousand-page Scheme report; setting aside the actual labour of designing and implementing it, most people would be perfectly content with using their own personal library of language extensions whose documentation would run to a thousand pages if it were all documented.<a href="#fn3" class="footnote-ref" id="fnref3" role="doc-noteref"><sup>3</sup></a> Our disagreement is over how big the base needs to be in order to provide a foundation to grow from; and over what needs to be in the foundation, to enable users to give themselves this bigness.</p>
<h2 id="dynamic-systems-static-languages">Dynamic systems, static languages</h2>
<p>So big vs little is one axis of disagreement within the Scheme community. How about another?</p>
<p>In 2012 Richard P. Gabriel wrote an essay paper, <a href="https://www.dreamsongs.com/Files/Incommensurability.pdf">‘The Structure of a Programming Language Revolution’</a>. He talks about how he took a break from programming languages in the early 1990s in order to do a fine arts degree. When he came back in the late 1990s, everything had changed. When he left, people in programming languages talked about dynamic, evolving systems like Common Lisp and Smalltalk. By the time he returned, the conversation had changed to static languages like Haskell.</p>
<p>I think one of the other axes of division in the Scheme community is around this matter, and the reason is that our community lived through this paradigm shift without, collectively, noticing it. Some of the community moved on to the new paradigm; others continued to think in terms of the old.<a href="#fn4" class="footnote-ref" id="fnref4" role="doc-noteref"><sup>4</sup></a> The two sides do not understand one another’s concerns.</p>
<p>The dynamic systems viewpoint is epitomized by the R5RS, whose only entrypoint for programs is the interactive top level, and which even began to lay the groundwork for a library and namespacing system that would have been based on first-class environments and the <code>eval</code> procedure. Implementations which follow the dynamic systems model include MIT Scheme and Guile.</p>
<p>The static languages viewpoint is epitomized by the R6RS, which doesn’t have an interactive top level at all. Implementations which adhere to this model include Chez, Racket, and Loko, but also niche implementations like Stalin which don’t have their own interactive top level at all.</p>
<p>Put briefly, the dynamic languages people see Scheme as a dynamic language, plain and simple, and want to be able to do all the run-time modification that they should be able to do in any such language. The static languages people see Scheme as a static language in all regards except its type system.</p>
<h2 id="and-yet-more">And yet more</h2>
<p>Already with these two dimensions of belief about what kind of language Scheme is – big vs little, static vs dynamic – we have a huge range of potential expectations for what the Scheme report might be like. When we compare these viewpoints in terms of the models their advocates might prefer to take as guidance for the future development of the language, we can see just how far apart these ideas are.</p>
<p>Those who want a little, dynamic language will look to earlier versions of the report, and maybe to the cores of languages like Lua and JavaScript. A little static language might look like SML. Common Lisp might be a model to follow for a big dynamic language, or Haskell for a big static language. I’ve seen almost every one of these languages used to suggest what a suitable future direction for Scheme might look like.</p>
<p>These are hugely different rôle models to follow – and these are only two matters on which people have different feelings. I could add more for more philosophical disagreements about the nature of Scheme, ideas about what the right way to make and publish a Scheme report should be, and even very specific individual language features. Clearly, we can’t satisfy all of these expectations at once.</p>
<h2 id="does-a-scheme-report-still-make-sense">Does a Scheme report still make sense?</h2>
<p>Given these deep divisions over the essential nature of the Scheme language, does it even make sense that we still keep making a Scheme report?</p>
<p>‘No’ is an entirely possible answer to this question. Already in the R6RS and R7RS small days, people were arguing that Scheme standardization should stop.</p>
<p>If we went this way then, just like Racket in its default mode no longer claims to be a Scheme report implementation, Schemes would slowly diverge into different languages. Guile Scheme would one day simply be Guile; Chicken Scheme would be Chicken, and so on. Like the many descendants of Algol 60 and 68, and the many dialects of those descendants, each of these languages would have a strongly recognizable common ancestor, but each would still be distinct and, ultimately, likely incompatible.</p>
<p>It shouldn’t surprise you that I don’t think this is the way to go. As a user of Scheme, I can still see ways I want to extend the common core of all of these implementations. It’s much better to be able to do this once and have my extension available on many implementations, than to have to work hard to port my extension to multiple different languages. All implementations benefit by agreeing on a common core that works to be able to grow the language.</p>
<p>Racket is already a cautionary tale. It has some amazing features, but those of us on other implementations can’t take advantage of them because Racket’s implementations of those features aren’t portable. Even Guile has this problem, because it’s still culturally normal among users of that implementation to use its own <code>define-module</code> declaration instead of the standard R6RS or R7RS <code>library</code> or <code>define-library</code>, even if your code only uses standard Scheme features and maybe some common SRFIs. There’s much Guile code I’d like to use on Chez for more speed, or on Chibi for a smaller implementation footprint, that currently requires at least some porting effort for not much good reason.</p>
<p>This is more or less the same rationale for Scheme reports that there has always been, and I think it continues to apply today.</p>
<h2 id="r6rs-and-r7rs-small-considered-more-alike-than-widely-believed">R6RS and R7RS small considered more alike than widely believed</h2>
<p>So our thoughts must naturally turn to unification. The co-existence and incompatibility of R6RS and R7RS small have become emblematic of the splits in our community. The R6RS editors and community are, to some extent, justified in feeling that R7RS small unfairly abandoned their work. But in fact the two are more similar than is widely recognized, and in many cases the design of R7RS small in fact vindicates the decisions of the R6RS editors.</p>
<p>This may seem a shocking claim about a division which some predicted would end up killing Scheme as a programming language. But, I think, WG1 stumbled their way towards a realization, which I’ll get to just below, which led them to a develop a library system working on basically the same model as R6RS. R7RS small library declarations support conditional compilation and other features which, in R6RS, is exclusively the province of the ‘main’ level language; but once all the <code>cond-expand</code>s in <code>define-library</code> are dealt with, the two are essentially the same. R7RS small’s <code>define-library</code> declarations can be compiled entirely ahead of time to R6RS’s <code>library</code> style – there is already a program which does this, <a href="https://akkuscm.org/">Akku</a>.</p>
<p>Between 2007 and 2013 the landscape of programming tools also shifted so some of the things which were criticized in R6RS’s day – bytevectors for binary data, and Unicode throughout – now seem perfectly normal. It now seems wrong for a programming language <em>not</em> to support these. R7RS small also adopted, for example, the same exception raising and handling mechanism as R6RS.<a href="#fn5" class="footnote-ref" id="fnref5" role="doc-noteref"><sup>5</sup></a></p>
<p>The changing landscape of programming tools around Scheme means that programmers now have a much different sense of the ‘littleness’ of a language than in 2007. Since Rust there’s increasing recognition that memory safety should be treated as non-optional even in most low-level programming, and is cheap to implement; it now seems strange that R5RS and below, and R7RS small, make bounds violations undefined behaviour in the C sense.<a href="#fn6" class="footnote-ref" id="fnref6" role="doc-noteref"><sup>6</sup></a></p>
<p>Moreover, what R7RS small’s authors seemed to realize is that the report will always be a minimum, and not a maximum. I think this is why, although it came from a group of users who started out highly critical of the R6RS for its static model of libraries and programs, R7RS small adopted essentially that model. It doesn’t restrict implementations from providing more dynamic library features.</p>
<p>The small/large report split helps even more, because now the report offers two different minimums for implementations to choose from, according to what their needs are. The minimum for ‘the practical needs of mainstream software development’ is, as Guy Steele says, much bigger than it used to be, and that’s fine!</p>
<h2 id="join-or-die">Join, or die</h2>
<p>This is my appeal to the Scheme community, then. If we don’t have a Scheme report that the majority of implementations and users accept, Scheme as a coherent single language will simply die. Not all implementations – it’ll be like if, as above, we stopped making Scheme reports and everyone went their own way. But even the implementations which survived would clearly be weaker for not being able to share the libraries of the others.</p>
<p>R7RS is our best shot at joining together to still be able to share code with one another, and to grow the language together – not only in the report itself, but much more in the tools we build with it.</p>
<h2 id="the-sacred-craft">The sacred craft</h2>
<p>Joining, however, means compromising.</p>
<p>Nobody will look at any Scheme report and say it is perfect. I can’t look at any version of the report, not even R5RS, and say it was a ‘diamond-like jewel’.<a href="#fn7" class="footnote-ref" id="fnref7" role="doc-noteref"><sup>7</sup></a> Even if it were perfect for one person, it cannot be all things to all people. Scheme has historically been too shy of compromise; the uncompromising pursuit of perfection, even though perfection is a quality nobody will ever agree on, may be why we have struggled so much to move onwards while other languages have not.</p>
<p>‘Compromise’ is a dirty word for some people, but I would like to offer another perspective. Although R7RS is sometimes perceived as anti-R6RS, two of the small report’s editors actually voted in favour of R6RS ratification.<a href="#fn8" class="footnote-ref" id="fnref8" role="doc-noteref"><sup>8</sup></a> One of them, John Cowan – also my predecessor as chair of WG2 for the large language – wrote in the rationale to his vote:</p>
<blockquote>
<p>a well-crafted compromise is in my opinion the most sacred thing that a secular age knows</p>
</blockquote>
<p>We may not be able to have a diamond-like jewel, but we can have the most sacred thing a secular age can make.</p>
<hr />
<p><i>As mentioned at the top, I hope this essay will really open a conversation about these matters. You can write a comment <a href="https://chaos.social/@dpk/115396196167588267">on Mastodon</a>, or if that’s too limiting, <a href="mailto:scheme-reports@scheme-reports.org?subject=Re%3A%20Scheme%20Reports%20at%20Fifty%3A%20Where%20do%20we%20go%20from%20here%3F&in-reply-to=%3CE94B0930-A69D-488F-936E-52118531D2F1%40nonceword.org%3E">on the Scheme Reports mailing list</a>.</i></p>
<p><i>And don’t forget the <a href="https://r7rs.org/sc/">election to the Scheme Steering Committee!</a></i></p>
<section id="footnotes" class="footnotes footnotes-end-of-document" role="doc-endnotes">
<hr />
<ol>
<li id="fn1"><p>26 people who voted against R6RS ratification said, in some form of words, that the language was too big; 13 people said they wanted specific new features and voted against because they weren’t there. Despite the apparent contradiction, the latter group overlaps with the former almost completely.<a href="#fnref1" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn2"><p>Ideology is not bad; I do not use the word to impugn anyone or their arguments. Without ideology, we have no structure with which to build our understanding of the world around us. I have my own ideology of Scheme as much as anyone in the community.<a href="#fnref2" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn3"><p>We may also need to set aside that only the person who wrote such a library would want to actually use it or any software written with it. Today we are also happy to shop in the free market of language extensions, choosing our own personal favourite design of pattern matcher or HTML templater out of many that have already been written by other people.<a href="#fnref3" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn4"><p>As an example of the kind of new work that came out of the Scheme community which didn’t make sense under the old paradigm but seems vital under the new, consider that it was only in 2002 that Matthew Flatt came out with <a href="https://www-old.cs.utah.edu/plt/publications/macromod.pdf">‘Composable and Compilable Macros: You Want it <em>When?</em>’</a>, introducing phasing as a model of compile-time evaluation compatible with static ahead-of-time compilation. This problem only began to be considered by Schemers in the 1990s, because the problem itself doesn’t really make sense under the dynamic, evolving systems paradigm.<a href="#fnref4" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn5"><p>Although the R7RS small authors didn’t seem to understand what <code>raise-continuable</code> was for and didn’t include an analogous construct to the non-<code>&amp;serious</code> condition types of R6RS.<a href="#fnref5" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn6"><p>My (least) favourite example is that it isn’t safe to decode UTF-8 text from an untrusted source in R7RS small, because invalid UTF-8 passed to <code>utf8-&gt;string</code> is also undefined behaviour. Anal monkeys galore.<a href="#fnref6" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn7"><p>What kind of diamond has undefined behaviour when you try and write beyond the end of a buffer? Does diamond-like perfection in a language inherently demand diamond-like perfection of the programs written in it?<a href="#fnref7" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn8"><p>Another WG1 member, Aaron Hsu, voted against R6RS ratification, but if one looks at his WG1 proposals and votes, later consistently pushed to adopt things from R6RS within WG1.<a href="#fnref8" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
</ol>
</section>]]></summary>
</entry>
<entry>
    <title>A story about data representation and performance</title>
    <link href="https://crumbles.blog/posts/2025-09-29-extensible-match-decision-tree-story.html" />
    <id>https://crumbles.blog/posts/2025-09-29-extensible-match-decision-tree-story.html</id>
    <published>2025-09-29T16:51:11Z</published>
    <updated>2025-09-29T16:51:11Z</updated>
    <summary type="html"><![CDATA[<p><img src="/images/racing-car.jpg" alt="" title="Things don’t actually lean forward, or even appear to lean forward, when they’re going fast in real life. This is an optical illusion that affects camera shutters, but not humans!"></p>
<p>If you go poking around the source code for <a href="https://codeberg.org/dpk/extensible-match">extensible-match</a>, you’ll find places where I define auxiliary syntax keywords and use syntax objects like records belonging to some sum type. Like <a href="https://codeberg.org/dpk/extensible-match/src/tag/v0.75/extensible-match/seq-pattern/common.sls#L47">here</a> or <a href="https://codeberg.org/dpk/extensible-match/src/tag/v0.75/extensible-match/seq-pattern/nfa.sls#L151">here</a> or <a href="https://codeberg.org/dpk/extensible-match/src/tag/v0.75/extensible-match/core-pattern.sls#L31">here</a>.</p>
<p>Extensible-match belongs to the comparatively rare class of Scheme macros that are complex enough that they count as compilers in themselves. As <a href="https://www.khoury.northeastern.edu/home/shivers/papers/loop.pdf">Olin says</a> when defending the use of records inside of his loop macro implementation, once you recognize that your macro has reached that level of complexity, it’s simply a matter of good programming practice to adjust your data representation and use records instead of trying to make ad hoc transformations on S-expressions. This goes up even to things that are themselves unambiguously compilers: the <a href="https://nanopass.org/">Nanopass framework</a> helped <a href="https://www.cambridge.org/core/journals/journal-of-functional-programming/article/educational-pearl-a-nanopass-framework-for-compiler-education/1E378B9B451270AF6A155FA0C21C04A3">compiler courses at Indiana</a> and then <a href="https://andykeep.com/pubs/dissertation.pdf">Chez Scheme itself</a> make the leap from trying to do everything with S-expressions to using proper records to represent and transform the input code. So why does extensible-match use these weird syntax object pseudo-records?</p>
<p>Well, there’s a story about that, and I consider myself to have learned my lesson here: eventually these things will probably mostly disappear.</p>
<p>Since extensible-match is a compiler, I thought when writing that I would benefit from being able to use a pattern matcher – but I also didn’t want to get into the mess that <a href="https://conservatory.scheme.org/psyntax/">psyntax</a> has where you have to use a pre-expanded version of the pattern matcher in order to be able to work on a new version. So I messily decided to abuse syntax objects as records to be able to consistently use <code>syntax-case</code> as the pattern matcher used to implement another, more general pattern matcher.</p>
<p>This came back to bite me quite badly.</p>
<p>The algorithm extensible-match uses to generate an optimal set of conditionals from patterns is <a href="http://moscova.inria.fr/~maranget/papers/ml05e-maranget.pdf">Luc Maranget’s decision tree algorithm</a>. The first problem that started to appear with using syntax objects here is that they don’t, in general, have a well-defined equivalence relation. For example, in order to reduce the number of cases the main decision tree generator has to deal with, there are some <a href="https://codeberg.org/dpk/extensible-match/src/tag/v0.75/extensible-match/decision-tree.sls#L353">‘source-level transformations’</a> which simplify some complex cases in the patterns themselves before generating the decision tree. One source-level transformation is rewriting disjunctive (<code>or</code>) subpatterns as separate top-level patterns. Another handles the case that a ‘row’ – essentially an <code>and</code> pattern where the order of checking subpatterns isn’t fixed – contains another row, by flattening the inner row into the outer row. But what if an <code>or</code> contains a row, or vice versa? The easiest thing to do is to iterate these source-level transformations to a fixed point: repeat the process, each time feeding last time’s output back in, until the output is the same as the input, at which point we know there’s nothing more that needs to be done. But you need an equivalence relation to be able to test if the output was the same as the input! So I had a pretty awful procedure that tried to tell if the transformation procedure <em>would</em> find any transformations to do on its input, which in any other circumstance would be the worst kind of premature optimization.</p>
<p>But the real problem came with the performance of Maranget’s algorithm. The algorithm has basically the same run-time complexity as the naïve recursive Fibonacci function: each step of generating the decision tree recurses on a set of patterns which is a little bit smaller than the input set, and on a set that’s a little bit smaller than that again. The hardest torture test I had when implementing Maranget’s algorithm was a port of the bytecode interpreter which is Example 3 in his paper. On Chez Scheme on a pretty recent MacBook, it took 8 seconds to generate the decision tree when I represented patterns this way. This is just 14 cases; Chez is one of the best-optimizing compilers around, and running on most implementations it would be even slower. 8 seconds for a moderately complex <code>match</code> is already unacceptable for incremental development – and once extensible-match starts supporting more Scheme implementations, for many people it would be even worse, since probably no other implementation would be as fast as Chez at this, and many people have slower computers than mine.</p>
<p>Finding the cause of this performance problem was a nightmare. It was a classic <a href="https://lobste.rs/s/y62lgp/flame_graphs_can_hide_small_overheads#c_azxicz">‘peanut butter’ performance problem</a>: because it was smeared out over each recursive step of the main algorithm, there was no obvious individual hot spot that was making it slow. Since I’ve already spoiled half the solution with the premise of this post, I’ll cut to the chase and spoil the rest: in order to distinguish which case of the (conceptual) sum type <code>syntax-case</code> is looking at, it uses the <code>free-identifier=?</code> comparision procedure. This procedure is actually quite slow because it has to check all the local bindings in scope where its inputs come from; since it’s rarely used intensively – usually only a few times per macro expansion, if even that – you don’t notice how slow it is until it starts to add up from being called again and again in the middle of an exponentially-recursive algorithm.</p>
<p>Ending appropriately spoiled, here’s the rest of the sorry story of me trying to work out what was wrong.</p>
<p>I initially thought it was because the hash consing of nodes wasn’t efficient enough (Chez’s <code>time</code> syntax told me that a lot of GCing was happening) – so I tried to optimize that, but it made no real difference. For all its strengths, Chez Scheme’s options for debugging and profiling are pretty weak: you can ask it to annotate code for profiling when loaded, but the resulting profile will only count how many times each bit of code was evaluated, not how much time the evaluation took. It also won’t tell you many times procedures hidden behind macro expansions are called (like <code>free-identifier=?</code> in <code>syntax-case</code>); nor will it go inside any code it hasn’t annotated or can’t annotate, like the procedures within the Chez Scheme standard library (like, again, <code>free-identifier=?</code>). Eventually it occurred to me that, while the whole extensible-match package wouldn’t load on Guile because Guile lacks identifier property support, the decision tree generator didn’t need that support, so I could at least use <a href="https://www.gnu.org/software/guile/manual/html_node/Statprof.html">its far superior profiling options</a> (a real statistical profiler! that also looks in the code you depend on and not only your own code!) to diagnose the problem.</p>
<p>That only confirmed my suspicions that 8 seconds in Chez would translate into something much, much worse on other implementations: the same 14 cases of Maranget’s bytecode interpreter took over 5 minutes to generate a decision tree on Guile! What’s more, although the profiler correctly identified <code>free-identifier=?</code> as the culprit, with 70% of execution time spent there, I couldn’t work out why. You see, the code does also explicitly use <code>free-identifier=?</code> to determine which predicate and accessor calls it can coalesce into a single node in the output decision tree. But I couldn’t believe that was the expensive part of this case!</p>
<p>I can’t remember what made me realize that it might be the <code>free-identifier=?</code> that was internal to the <code>syntax-case</code> form that discriminated between different types of patterns that might be issue, but since <code>free-identifier=?</code> seemed to be the culprit, I started to work on getting it out of the main decision tree generation procedure entirely: not only the explicit uses, but the implicit use in <code>syntax-case</code> as well. I had a <a href="https://codeberg.org/dpk/extensible-match/commit/c807467911a988041fcbde382951883b3bd0e138">very painful refactoring session</a> adding a new layer to the representation of patterns, converting their representation from syntax objects to real Scheme records. <code>Free-identifier=?</code> during decision tree generation was replaced by interning patterns’ embedded expressions into a table so instances of the same identifier, which could be coalesced into a single node in the output decision tree, could be checked by a simple integer comparison instead of <code>free-identifier=?</code>’s recursive scope analysis. (Building this table has quadratic complexity since Scheme has no <code>free-identifier-hash</code>, but it should still save time.)</p>
<p>This refactoring meant I could no longer use the <code>syntax-case</code> pattern matcher to implement the decision tree generator – but there was one nice side-effect, because I could use the opportunity to make explicit one bit of state that was only implicit in the old representation: the name of the variable which has the actual value each subpattern is being tested against.</p>
<p>Whoosh! That alone solved the performance problem: from 8 seconds, the decision tree generation for Maranget’s interpreter was down to 0.2 seconds in Chez. On Guile it’s still 9.3 seconds – although this can’t fairly be compared with the 314 seconds it took there before, as I have a newer and slightly faster computer since I originally ran that benchmark, and (for whatever reason) I didn’t repeat this performance test on Guile on the old computer once I’d solved the performance issue.</p>
<p>Ten seconds for 14 cases still isn’t great, but I’m still much less concerned about this. If you recall my mention of the similarity to a Fibonacci recursion above, it might seem that the solution is memoization. In theory, memoizing the recursion over the smaller of the two cases – the one which excludes the topmost row, which generates the side of the tree used when a test required by that row failed – might help. I haven’t tried this yet, though: based on anecdotal evidence, 14 cases is already larger than the vast majority of pattern matches out there, and 0.2 seconds for this on the main implementation extensible-match supports is pretty okay.</p>
<p>If I do go in and start looking for expand-time performance improvements again, another option for faster compilation might be to just split the input pattern list into chunks, and give up on optimizing the decision tree between the chunks. Since the performance characteristic is exponential and <span class="math inline"><em>k</em><sup><em>n</em></sup> + <em>k</em><sup><em>m</em></sup></span> is usually much smaller than <span class="math inline"><em>k</em><sup><em>n</em> + <em>m</em></sup></span>, this indeed seems to work: splitting these 14 cases up into 10 + 4 cases gets a decision tree in under a second on Guile – but this feels like cheating. Maybe with a compile-time option to set the chunk size or disable the cheat (so production builds get fully-optimized, fast-running decision trees while development builds get fast compilation but slower matching) … I’ll probably wait until match compilation speed on Guile isn’t quite such a shitshow for other, unrelated reasons before starting to really consider these options. (For one thing, how do <code>or</code> patterns count here? As mentioned above, they hide multiple cases, potentially buried deep inside a nested subpattern, and have to be unfolded to the top level to get a true count of the number of cases the decision tree generator will see.)</p>
<p>Anyway, the point here is the main representation of patterns is now in real Scheme records, as God intended, but there are other places in the code that still abuse syntax objects as records. Those don’t hit any performance-sensitive code, though. Probably it would be better to refactor them out as well, some day: you might notice that this isn’t the only place where records which start out as syntax objects get turned into <a href="https://codeberg.org/dpk/extensible-match/src/tag/v0.75/extensible-match/seq-pattern/nfa.sls#L244">another layer using real records</a> (oops, bad idiom for sum types there – again, I wanted to be able to use <code>case</code> and not a load of type test predicates in a <code>cond</code>, which was really very petty of me :-( ). But that concludes today’s missive. Moral of the story: use real records in your macros when you need to; don’t try and fake them with syntax objects because you think <code>syntax-case</code> will be a nice pattern matcher to implement your compiler with. If you’re implementing anything other than another pattern matcher, you should have no reason to even think of abusing <code>syntax-case</code> for this in the first place – just use a real pattern matcher like extensible-match inside of your macro definition!</p>
<p><strong>Fun fact:</strong> Although this is the first post on this new blog, it’s actually a footnote to another blog post I started to write. The footnote got … rather out of hand, and ended up being a post of its own.</p>
<hr />
<p><span id="update-3-october"></span><strong>Update, 3 October:</strong> Zhu Zihao asked <em>why</em> it is that <code>free-identifier=?</code> is so slow; he asked if changing the representation of ribcages in syntax objects to something like a hash table (from an alist) might help. Well, really, an alternative way to look at it isn’t so much that <code>free-identifier=?</code> is slow – it’s more just that normal record type checking is really fast. Since none of the record types that are actually used in representing the AST of patterns are subtyped, a successful type check involves only dereferencing the pointer to the pattern itself then checking pointer equality for the heap tag. Since I didn’t mark any of these record types as sealed, an unsuccessful check is at least another size check (size of the record type’s ancestry) and dereference (the vector of its ancestors) and pointer comparison (the type in the RTD’s place in the ancestry vector), but that’s still minimal, and pretty cache-friendly to boot. (Maybe I could squeeze an extra fraction of a fraction of a second of performance out by declaring these types sealed to avoid all that.)</p>
<p>By comparison, <em>any</em> amount of checking which of multiple bindings the identifier-abused-as-type-tag might have is always going to be slower than this. Since <a href="https://funcall.blogspot.com/2016/01/alist-vs-hash-table.html">alists are faster than hash tables up to about 25 entries</a> (depending on Scheme implementation, measurement technique, etc.) it’s not even clear there would be any win to be had in that direction.</p>
<p>Besides, another source of speed-up I didn’t mention in this post was field access. Field access with the syntax object representation involved chasing pointers, checking the wrapped list was the right length (even though it always would be, by construction) … and now, like the heap tag test, it’s always a simple, safe pointer dereference. The 70% number from Guile’s profiler gives an impression of the ratio of how much performance improvement came from getting rid of <code>free-identifier=?</code> and how much from not having to chase pointers down a list for field access.</p>
<p>8 seconds to 0.2 seconds is, actually, ‘only’ a speed-up factor of 40. While it sounds like a lot, I find it prima facie utterly believable that memory accesses and branches went down by this factor due to this change in data representation: if I think about the difference in what the computer is doing for each, it makes total sense.</p>]]></summary>
</entry>

</feed>
