<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://lmilz.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://lmilz.dev/" rel="alternate" type="text/html" /><updated>2026-08-03T19:40:11+00:00</updated><id>https://lmilz.dev/feed.xml</id><title type="html">Lars Milz</title><subtitle>Software Engineer &amp; Father</subtitle><entry><title type="html">Why the Sci-Fi Quantum Computer Fails at the Weather</title><link href="https://lmilz.dev/blog/2026/07/11/Why-the-Sci-Fi-Quantum-Computer-Fails-at-the-Weather.html" rel="alternate" type="text/html" title="Why the Sci-Fi Quantum Computer Fails at the Weather" /><published>2026-07-11T00:00:00+00:00</published><updated>2026-07-11T00:00:00+00:00</updated><id>https://lmilz.dev/blog/2026/07/11/Why-the-Sci-Fi-Quantum-Computer-Fails-at-the-Weather</id><content type="html" xml:base="https://lmilz.dev/blog/2026/07/11/Why-the-Sci-Fi-Quantum-Computer-Fails-at-the-Weather.html"><![CDATA[<p>I recently read <em>Der Zorn des Oktopus</em> (“The Wrath of the Octopus”) by Dirk Rossmann and Ralf Hoppe, a novel in which a quantum computer is built to predict chaotic systems. The basic idea: determine weather events once it has “all the data”. If x is supposed to happen, a causal chain y has to occur first, and a sufficiently large quantum computer simply plays that chain forward. In the novel the machine is promptly abused to influence events: what has to happen so that event X takes place?</p>

<p>Reading this left me confused, because quantum computers, as they stand today, are not suited for this kind of computation. Yet the motif is popular in science fiction. Adam Fawer’s thriller <em>Improbable</em> has no quantum computer, but there someone turns into a so-called Laplace’s demon: an intelligence, conceived by Pierre-Simon Laplace in 1814, that knows the exact state of every particle in the universe and computes the entire future and past from it. The punchline: long before computing power becomes the issue, the demon fails at his input, because “all the data” is not something that physically exists (more on that below). So the half-sentence “once it has all the data” does not skip some minor detail, it skips exactly the spot where physics says no. Which made me ask, also because I had not followed the state of research for over ten years: are there quantum algorithms for chaotic systems at all, and where exactly does the idea break in practice?</p>

<h2 id="the-basic-problem-linear-versus-nonlinear">The basic problem: linear versus nonlinear</h2>

<p>A quantum computer computes in a fundamentally different way from a classical machine. Instead of bits (0 or 1) it works with qubits, which span huge state spaces through superposition and entanglement. The decisive catch for the weather scenario is the mathematics underneath: quantum mechanical time evolution under the Schrödinger equation is necessarily linear and unitary, probabilities are preserved.</p>

<p>The weather, however, is a highly chaotic system. Chaotic systems are deterministic and still unpredictable in the long run. They are described by nonlinear equations, such as the Navier-Stokes equations for turbulent flows or the simplified Lorenz model. Their defining feature is the butterfly effect: extreme sensitivity to initial conditions. If two initial states differ by as little as a millionth of a degree, their distance grows exponentially over time. This divergence is measured by the <strong>positive Lyapunov exponent</strong>. Even if the quantum computer were infinitely fast, the smallest measurement error in the input data blows up any exact trajectory prediction after a short time (the Lyapunov time).</p>

<p>So we are forcing a machine that only masters linear operations to solve a nonlinear problem. The currently most studied way to bridge this gap is called Carleman linearization.</p>

<h2 id="carleman-linearization-in-one-equation">Carleman linearization in one equation</h2>

<p>The idea is old and elegant. You embed a nonlinear equation into a linear system by treating the powers of the variables as new, independent variables. The clearest way to see it is the simplest nonlinear example, the logistic equation:</p>

\[\frac{dx}{dt} = x(1 - x) = x - x^2\]

<p>Now define \(y_k := x^k\) and differentiate:</p>

\[\frac{dy_k}{dt} = k\,x^{k-1}\frac{dx}{dt} = k\,x^{k-1}(x - x^2) = k\,(x^k - x^{k+1}) = k\,(y_k - y_{k+1})\]

<p>That is the whole mechanism. A nonlinear equation becomes a <strong>linear</strong> system, but one with infinitely many equations: \(y_1\) depends on \(y_2\), \(y_2\) on \(y_3\), and so on. In vector form this is \(\dot{\mathbf{y}} = A\,\mathbf{y}\) with an infinitely large, sparse matrix \(A\). And that is exactly the form a quantum computer likes: linear systems of differential equations can be handled by algorithms like HHL or newer linear solvers in time that grows only logarithmically with the dimension.</p>

<p>The catch: nobody can solve infinitely many equations. You truncate at some order \(N\) and set \(y_{N+1} = 0\). The last row then becomes \(\dot{y}_N = N\,y_N\), a pure growth term. The eigenvalues of the truncated matrix are simply \(1, 2, \dots, N\), all positive. Keep that in mind for a moment.</p>

<h2 id="the-experiment">The experiment</h2>

<p>How quickly this truncation breaks down is something an experiment can show. Here is the complete code: it solves the logistic equation exactly (analytically) and compares it with Carleman truncations of different orders, solved via the matrix exponential.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">numpy</span> <span class="k">as</span> <span class="n">np</span>
<span class="kn">from</span> <span class="n">scipy.linalg</span> <span class="kn">import</span> <span class="n">expm</span>

<span class="k">def</span> <span class="nf">carleman_matrix</span><span class="p">(</span><span class="n">N</span><span class="p">):</span>
    <span class="sh">"""</span><span class="s">Carleman matrix for dx/dt = x - x^2 with y_k = x^k.
    dy_k/dt = k*(y_k - y_{k+1}). Truncation: y_{N+1} = 0.</span><span class="sh">"""</span>
    <span class="n">A</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">zeros</span><span class="p">((</span><span class="n">N</span><span class="p">,</span> <span class="n">N</span><span class="p">))</span>
    <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">N</span> <span class="o">+</span> <span class="mi">1</span><span class="p">):</span>
        <span class="n">A</span><span class="p">[</span><span class="n">k</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="n">k</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="n">k</span>
        <span class="k">if</span> <span class="n">k</span> <span class="o">&lt;</span> <span class="n">N</span><span class="p">:</span>
            <span class="n">A</span><span class="p">[</span><span class="n">k</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="n">k</span><span class="p">]</span> <span class="o">=</span> <span class="o">-</span><span class="n">k</span>
    <span class="k">return</span> <span class="n">A</span>

<span class="k">def</span> <span class="nf">exact</span><span class="p">(</span><span class="n">x0</span><span class="p">,</span> <span class="n">t</span><span class="p">):</span>
    <span class="k">return</span> <span class="n">x0</span> <span class="o">/</span> <span class="p">(</span><span class="n">x0</span> <span class="o">+</span> <span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">x0</span><span class="p">)</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="nf">exp</span><span class="p">(</span><span class="o">-</span><span class="n">t</span><span class="p">))</span>

<span class="k">def</span> <span class="nf">carleman_solve</span><span class="p">(</span><span class="n">x0</span><span class="p">,</span> <span class="n">t</span><span class="p">,</span> <span class="n">N</span><span class="p">):</span>
    <span class="n">A</span> <span class="o">=</span> <span class="nf">carleman_matrix</span><span class="p">(</span><span class="n">N</span><span class="p">)</span>
    <span class="n">y0</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">array</span><span class="p">([</span><span class="n">x0</span><span class="o">**</span><span class="n">k</span> <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">N</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)])</span>
    <span class="k">return</span> <span class="n">np</span><span class="p">.</span><span class="nf">array</span><span class="p">([(</span><span class="nf">expm</span><span class="p">(</span><span class="n">A</span> <span class="o">*</span> <span class="n">tt</span><span class="p">)</span> <span class="o">@</span> <span class="n">y0</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span> <span class="k">for</span> <span class="n">tt</span> <span class="ow">in</span> <span class="n">t</span><span class="p">])</span>

<span class="n">t</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">linspace</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mf">1.6</span><span class="p">,</span> <span class="mi">500</span><span class="p">)</span>
<span class="n">x0</span> <span class="o">=</span> <span class="mf">0.6</span>
<span class="n">ex</span> <span class="o">=</span> <span class="nf">exact</span><span class="p">(</span><span class="n">x0</span><span class="p">,</span> <span class="n">t</span><span class="p">)</span>
<span class="k">for</span> <span class="n">N</span> <span class="ow">in</span> <span class="p">[</span><span class="mi">2</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">16</span><span class="p">]:</span>
    <span class="n">cl</span> <span class="o">=</span> <span class="nf">carleman_solve</span><span class="p">(</span><span class="n">x0</span><span class="p">,</span> <span class="n">t</span><span class="p">,</span> <span class="n">N</span><span class="p">)</span>
    <span class="n">err</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">abs</span><span class="p">(</span><span class="n">cl</span> <span class="o">-</span> <span class="n">ex</span><span class="p">)</span>
    <span class="n">idx</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">argmax</span><span class="p">(</span><span class="n">err</span> <span class="o">&gt;</span> <span class="mf">1e-2</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">N=</span><span class="si">{</span><span class="n">N</span><span class="si">:</span><span class="mi">2</span><span class="n">d</span><span class="si">}</span><span class="s">: breaks down at t=</span><span class="si">{</span><span class="n">t</span><span class="p">[</span><span class="n">idx</span><span class="p">]</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="n">f</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
</code></pre></div></div>

<p>The result as a picture:</p>

<p><img src="/assets/images/carleman_logistic.png" alt="Carleman linearization of the logistic equation" /></p>

<p>Two things stand out.</p>

<p>First: every truncation follows the exact solution perfectly for a while and then tips over abruptly. That is not a rounding error, it is structural. As soon as the truncated order becomes relevant, the growth term \(\dot{y}_N = N\,y_N\) from above takes over and the solution explodes. In the code you see maximum errors of \(10^{30}\) and beyond.</p>

<p>Second: higher order helps, but with clearly diminishing returns. Going from order 2 to 16, eight times the number of variables, only shifts the valid window from \(t \approx 0.2\) to \(t \approx 0.82\).</p>

<h2 id="why-the-window-does-not-grow-arbitrarily">Why the window does not grow arbitrarily</h2>

<p>The second point is the decisive one. There is a parameter that determines whether Carleman converges at all. Liu and colleagues showed in 2021 that the ratio \(R\) of nonlinearity to dissipative linearity decides the situation: for \(R &lt; 1\) there is a genuine quantum advantage, for \(R \ge \sqrt{2}\) the problem is not efficiently solvable in the worst case<sup id="fnref:liu2021"><a href="#fn:liu2021" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>. The logistic equation has a growing linear part instead of a damping one, so it sits deliberately in the hard regime, hence the early breakdown.</p>

<p>In fluid dynamics this \(R\) carries a familiar name. Several works link it to the Reynolds number, which measures precisely the ratio of inertial to viscous forces and thus how turbulent a flow is<sup id="fnref:tennie2025"><a href="#fn:tennie2025" class="footnote" rel="footnote" role="doc-noteref">2</a></sup>. Gonzalez-Conde and colleagues tie the convergence to the Kolmogorov scale, the grid resolution needed to resolve the energy cascade of the nonlinear term<sup id="fnref:gonzalez2025"><a href="#fn:gonzalez2025" class="footnote" rel="footnote" role="doc-noteref">3</a></sup>. Sanavio and Succi get their lattice-Boltzmann-Carleman variant to work at moderate Reynolds numbers between 10 and 100 already at second order<sup id="fnref:sanavio2024"><a href="#fn:sanavio2024" class="footnote" rel="footnote" role="doc-noteref">4</a></sup>. That is encouraging, but it does not describe real turbulence yet.</p>

<h2 id="from-the-principle-to-real-chaos-the-lorenz-system">From the principle to real chaos: the Lorenz system</h2>

<p>The logistic equation demonstrates the mechanism, but it is only one-dimensional and not chaotic. For a well-known chaotic system with several variables, consider the Lorenz system:</p>

\[\frac{dx}{dt} = \sigma(y - x)\]

\[\frac{dy}{dt} = x(\rho - z) - y\]

\[\frac{dz}{dt} = xy - \beta z\]

<p>with the classic parameters \(\sigma = 10\), \(\rho = 28\), \(\beta = 8/3\). The system has three variables and shows real deterministic chaos with the famous butterfly attractor.</p>

<p>Carleman linearization works analogously: you define monomials \(x^a y^b z^c\) as new variables and derive the linear system in them. At truncation order \(N\) (all monomials with \(a+b+c \leq N\)) the dimension grows quickly: \(N=2\) needs 9 variables, \(N=3\) already 19.</p>

<p>Here is the code:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">numpy</span> <span class="k">as</span> <span class="n">np</span>
<span class="kn">from</span> <span class="n">scipy.linalg</span> <span class="kn">import</span> <span class="n">expm</span>

<span class="k">def</span> <span class="nf">generate_monomials</span><span class="p">(</span><span class="n">N</span><span class="p">):</span>
    <span class="sh">"""</span><span class="s">All monomials x^a y^b z^c with a+b+c &lt;= N.</span><span class="sh">"""</span>
    <span class="n">monoms</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">total</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">N</span><span class="o">+</span><span class="mi">1</span><span class="p">):</span>
        <span class="k">for</span> <span class="n">a</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">total</span><span class="o">+</span><span class="mi">1</span><span class="p">):</span>
            <span class="k">for</span> <span class="n">b</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">total</span><span class="o">+</span><span class="mi">1</span><span class="o">-</span><span class="n">a</span><span class="p">):</span>
                <span class="n">c</span> <span class="o">=</span> <span class="n">total</span> <span class="o">-</span> <span class="n">a</span> <span class="o">-</span> <span class="n">b</span>
                <span class="n">monoms</span><span class="p">.</span><span class="nf">append</span><span class="p">((</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">c</span><span class="p">))</span>
    <span class="k">return</span> <span class="n">monoms</span>

<span class="k">def</span> <span class="nf">deriv_monom</span><span class="p">(</span><span class="n">abc</span><span class="p">,</span> <span class="n">sigma</span><span class="o">=</span><span class="mf">10.0</span><span class="p">,</span> <span class="n">rho</span><span class="o">=</span><span class="mf">28.0</span><span class="p">,</span> <span class="n">beta</span><span class="o">=</span><span class="mf">8.0</span><span class="o">/</span><span class="mf">3.0</span><span class="p">):</span>
    <span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">c</span> <span class="o">=</span> <span class="n">abc</span>
    <span class="n">terms</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">if</span> <span class="n">a</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">:</span>
        <span class="n">terms</span><span class="p">.</span><span class="nf">append</span><span class="p">((</span><span class="n">a</span> <span class="o">*</span> <span class="n">sigma</span><span class="p">,</span> <span class="p">(</span><span class="n">a</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="n">b</span><span class="o">+</span><span class="mi">1</span><span class="p">,</span> <span class="n">c</span><span class="p">)))</span>
        <span class="n">terms</span><span class="p">.</span><span class="nf">append</span><span class="p">((</span><span class="o">-</span><span class="n">a</span> <span class="o">*</span> <span class="n">sigma</span><span class="p">,</span> <span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">c</span><span class="p">)))</span>
    <span class="k">if</span> <span class="n">b</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">:</span>
        <span class="n">terms</span><span class="p">.</span><span class="nf">append</span><span class="p">((</span><span class="n">b</span> <span class="o">*</span> <span class="n">rho</span><span class="p">,</span> <span class="p">(</span><span class="n">a</span><span class="o">+</span><span class="mi">1</span><span class="p">,</span> <span class="n">b</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="n">c</span><span class="p">)))</span>
        <span class="n">terms</span><span class="p">.</span><span class="nf">append</span><span class="p">((</span><span class="o">-</span><span class="n">b</span><span class="p">,</span> <span class="p">(</span><span class="n">a</span><span class="o">+</span><span class="mi">1</span><span class="p">,</span> <span class="n">b</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="n">c</span><span class="o">+</span><span class="mi">1</span><span class="p">)))</span>
        <span class="n">terms</span><span class="p">.</span><span class="nf">append</span><span class="p">((</span><span class="o">-</span><span class="n">b</span><span class="p">,</span> <span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">c</span><span class="p">)))</span>
    <span class="k">if</span> <span class="n">c</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">:</span>
        <span class="n">terms</span><span class="p">.</span><span class="nf">append</span><span class="p">((</span><span class="n">c</span><span class="p">,</span> <span class="p">(</span><span class="n">a</span><span class="o">+</span><span class="mi">1</span><span class="p">,</span> <span class="n">b</span><span class="o">+</span><span class="mi">1</span><span class="p">,</span> <span class="n">c</span><span class="o">-</span><span class="mi">1</span><span class="p">)))</span>
        <span class="n">terms</span><span class="p">.</span><span class="nf">append</span><span class="p">((</span><span class="o">-</span><span class="n">c</span> <span class="o">*</span> <span class="n">beta</span><span class="p">,</span> <span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">c</span><span class="p">)))</span>
    <span class="k">return</span> <span class="n">terms</span>

<span class="k">def</span> <span class="nf">carleman_matrix_lorenz</span><span class="p">(</span><span class="n">N</span><span class="p">,</span> <span class="n">sigma</span><span class="o">=</span><span class="mf">10.0</span><span class="p">,</span> <span class="n">rho</span><span class="o">=</span><span class="mf">28.0</span><span class="p">,</span> <span class="n">beta</span><span class="o">=</span><span class="mf">8.0</span><span class="o">/</span><span class="mf">3.0</span><span class="p">):</span>
    <span class="n">monoms</span> <span class="o">=</span> <span class="nf">generate_monomials</span><span class="p">(</span><span class="n">N</span><span class="p">)</span>
    <span class="n">idx</span> <span class="o">=</span> <span class="p">{</span><span class="n">abc</span><span class="p">:</span> <span class="n">i</span> <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">abc</span> <span class="ow">in</span> <span class="nf">enumerate</span><span class="p">(</span><span class="n">monoms</span><span class="p">)}</span>
    <span class="n">n</span> <span class="o">=</span> <span class="nf">len</span><span class="p">(</span><span class="n">monoms</span><span class="p">)</span>
    <span class="n">A</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">zeros</span><span class="p">((</span><span class="n">n</span><span class="p">,</span> <span class="n">n</span><span class="p">))</span>
    <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">abc</span> <span class="ow">in</span> <span class="nf">enumerate</span><span class="p">(</span><span class="n">monoms</span><span class="p">):</span>
        <span class="k">for</span> <span class="n">coeff</span><span class="p">,</span> <span class="n">abc_prime</span> <span class="ow">in</span> <span class="nf">deriv_monom</span><span class="p">(</span><span class="n">abc</span><span class="p">,</span> <span class="n">sigma</span><span class="p">,</span> <span class="n">rho</span><span class="p">,</span> <span class="n">beta</span><span class="p">):</span>
            <span class="k">if</span> <span class="nf">sum</span><span class="p">(</span><span class="n">abc_prime</span><span class="p">)</span> <span class="o">&lt;=</span> <span class="n">N</span> <span class="ow">and</span> <span class="n">abc_prime</span> <span class="ow">in</span> <span class="n">idx</span><span class="p">:</span>
                <span class="n">A</span><span class="p">[</span><span class="n">i</span><span class="p">,</span> <span class="n">idx</span><span class="p">[</span><span class="n">abc_prime</span><span class="p">]]</span> <span class="o">+=</span> <span class="n">coeff</span>
    <span class="k">return</span> <span class="n">A</span><span class="p">,</span> <span class="n">monoms</span>

<span class="k">def</span> <span class="nf">exact_lorenz_rk4</span><span class="p">(</span><span class="n">y0</span><span class="p">,</span> <span class="n">t</span><span class="p">,</span> <span class="n">sigma</span><span class="o">=</span><span class="mf">10.0</span><span class="p">,</span> <span class="n">rho</span><span class="o">=</span><span class="mf">28.0</span><span class="p">,</span> <span class="n">beta</span><span class="o">=</span><span class="mf">8.0</span><span class="o">/</span><span class="mf">3.0</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">rhs</span><span class="p">(</span><span class="n">s</span><span class="p">):</span>
        <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">z</span> <span class="o">=</span> <span class="n">s</span>
        <span class="k">return</span> <span class="n">np</span><span class="p">.</span><span class="nf">array</span><span class="p">([</span><span class="n">sigma</span><span class="o">*</span><span class="p">(</span><span class="n">y</span><span class="o">-</span><span class="n">x</span><span class="p">),</span> <span class="n">x</span><span class="o">*</span><span class="p">(</span><span class="n">rho</span><span class="o">-</span><span class="n">z</span><span class="p">)</span><span class="o">-</span><span class="n">y</span><span class="p">,</span> <span class="n">x</span><span class="o">*</span><span class="n">y</span><span class="o">-</span><span class="n">beta</span><span class="o">*</span><span class="n">z</span><span class="p">])</span>
    <span class="n">states</span> <span class="o">=</span> <span class="p">[</span><span class="n">y0</span><span class="p">]</span>
    <span class="n">dt</span> <span class="o">=</span> <span class="n">t</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">-</span> <span class="n">t</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="nf">len</span><span class="p">(</span><span class="n">t</span><span class="p">)</span><span class="o">-</span><span class="mi">1</span><span class="p">):</span>
        <span class="n">s</span> <span class="o">=</span> <span class="n">states</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span>
        <span class="n">k1</span> <span class="o">=</span> <span class="nf">rhs</span><span class="p">(</span><span class="n">s</span><span class="p">)</span>
        <span class="n">k2</span> <span class="o">=</span> <span class="nf">rhs</span><span class="p">(</span><span class="n">s</span> <span class="o">+</span> <span class="mf">0.5</span><span class="o">*</span><span class="n">dt</span><span class="o">*</span><span class="n">k1</span><span class="p">)</span>
        <span class="n">k3</span> <span class="o">=</span> <span class="nf">rhs</span><span class="p">(</span><span class="n">s</span> <span class="o">+</span> <span class="mf">0.5</span><span class="o">*</span><span class="n">dt</span><span class="o">*</span><span class="n">k2</span><span class="p">)</span>
        <span class="n">k4</span> <span class="o">=</span> <span class="nf">rhs</span><span class="p">(</span><span class="n">s</span> <span class="o">+</span> <span class="n">dt</span><span class="o">*</span><span class="n">k3</span><span class="p">)</span>
        <span class="n">states</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">s</span> <span class="o">+</span> <span class="p">(</span><span class="n">dt</span><span class="o">/</span><span class="mi">6</span><span class="p">)</span><span class="o">*</span><span class="p">(</span><span class="n">k1</span> <span class="o">+</span> <span class="mi">2</span><span class="o">*</span><span class="n">k2</span> <span class="o">+</span> <span class="mi">2</span><span class="o">*</span><span class="n">k3</span> <span class="o">+</span> <span class="n">k4</span><span class="p">))</span>
    <span class="k">return</span> <span class="n">np</span><span class="p">.</span><span class="nf">array</span><span class="p">(</span><span class="n">states</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">carleman_solve_lorenz</span><span class="p">(</span><span class="n">y0</span><span class="p">,</span> <span class="n">t</span><span class="p">,</span> <span class="n">N</span><span class="p">,</span> <span class="n">sigma</span><span class="o">=</span><span class="mf">10.0</span><span class="p">,</span> <span class="n">rho</span><span class="o">=</span><span class="mf">28.0</span><span class="p">,</span> <span class="n">beta</span><span class="o">=</span><span class="mf">8.0</span><span class="o">/</span><span class="mf">3.0</span><span class="p">):</span>
    <span class="n">A</span><span class="p">,</span> <span class="n">monoms</span> <span class="o">=</span> <span class="nf">carleman_matrix_lorenz</span><span class="p">(</span><span class="n">N</span><span class="p">,</span> <span class="n">sigma</span><span class="p">,</span> <span class="n">rho</span><span class="p">,</span> <span class="n">beta</span><span class="p">)</span>
    <span class="n">idx</span> <span class="o">=</span> <span class="p">{</span><span class="n">abc</span><span class="p">:</span> <span class="n">i</span> <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">abc</span> <span class="ow">in</span> <span class="nf">enumerate</span><span class="p">(</span><span class="n">monoms</span><span class="p">)}</span>
    <span class="n">y0_carleman</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">array</span><span class="p">([</span><span class="n">y0</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">**</span><span class="n">a</span> <span class="o">*</span> <span class="n">y0</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="o">**</span><span class="n">b</span> <span class="o">*</span> <span class="n">y0</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span><span class="o">**</span><span class="n">c</span> <span class="k">for</span> <span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">monoms</span><span class="p">])</span>
    <span class="n">states</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">tt</span> <span class="ow">in</span> <span class="n">t</span><span class="p">:</span>
        <span class="n">y_t</span> <span class="o">=</span> <span class="nf">expm</span><span class="p">(</span><span class="n">A</span> <span class="o">*</span> <span class="n">tt</span><span class="p">)</span> <span class="o">@</span> <span class="n">y0_carleman</span>
        <span class="n">states</span><span class="p">.</span><span class="nf">append</span><span class="p">([</span><span class="n">y_t</span><span class="p">[</span><span class="n">idx</span><span class="p">[(</span><span class="mi">1</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">)]],</span> <span class="n">y_t</span><span class="p">[</span><span class="n">idx</span><span class="p">[(</span><span class="mi">0</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">0</span><span class="p">)]],</span> <span class="n">y_t</span><span class="p">[</span><span class="n">idx</span><span class="p">[(</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">1</span><span class="p">)]]])</span>
    <span class="k">return</span> <span class="n">np</span><span class="p">.</span><span class="nf">array</span><span class="p">(</span><span class="n">states</span><span class="p">)</span>

<span class="c1"># main run
</span><span class="n">y0</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">array</span><span class="p">([</span><span class="mf">1.0</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">])</span>
<span class="n">t</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">linspace</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mf">2.0</span><span class="p">,</span> <span class="mi">500</span><span class="p">)</span>
<span class="n">ex</span> <span class="o">=</span> <span class="nf">exact_lorenz_rk4</span><span class="p">(</span><span class="n">y0</span><span class="p">,</span> <span class="n">t</span><span class="p">)</span>
<span class="k">for</span> <span class="n">N</span> <span class="ow">in</span> <span class="p">[</span><span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">]:</span>
    <span class="n">cl</span> <span class="o">=</span> <span class="nf">carleman_solve_lorenz</span><span class="p">(</span><span class="n">y0</span><span class="p">,</span> <span class="n">t</span><span class="p">,</span> <span class="n">N</span><span class="p">)</span>
    <span class="n">err</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">linalg</span><span class="p">.</span><span class="nf">norm</span><span class="p">(</span><span class="n">cl</span> <span class="o">-</span> <span class="n">ex</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
    <span class="n">idx</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">argmax</span><span class="p">(</span><span class="n">err</span> <span class="o">&gt;</span> <span class="mf">10.0</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">idx</span> <span class="o">==</span> <span class="mi">0</span> <span class="ow">and</span> <span class="n">err</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">&lt;=</span> <span class="mf">10.0</span><span class="p">:</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">N=</span><span class="si">{</span><span class="n">N</span><span class="si">}</span><span class="s">: stable until t=</span><span class="si">{</span><span class="n">t</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="n">f</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">N=</span><span class="si">{</span><span class="n">N</span><span class="si">}</span><span class="s">: breaks down (error &gt; 10) at t=</span><span class="si">{</span><span class="n">t</span><span class="p">[</span><span class="n">idx</span><span class="p">]</span><span class="si">:</span><span class="p">.</span><span class="mi">3</span><span class="n">f</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
</code></pre></div></div>

<p>The result for the initial value \((1, 1, 1)\):</p>

<p><img src="/assets/images/carleman_lorenz.png" alt="Carleman linearization of the Lorenz system" /></p>

<p>x(t), y(t), z(t) and the error on a log scale. The exact RK4 solution (black) stays bounded, the Carleman approximations (orange N=2, blue N=3) explode from t ≈ 0.3 on. The x marker shows the respective divergence point, the red line the error threshold of 10.</p>

<p>The difference to the logistic equation is drastic. Higher order barely helps here: from \(N=2\) to \(N=3\) the valid window only shifts from \(t \approx 0.27\) to \(t \approx 0.29\). For real chaos, the Carleman truncation is next to useless.</p>

<p><img src="/assets/images/carleman_lorenz_3d.png" alt="Lorenz attractor in 3D, exact RK4 solution over t = 0 to 40" /></p>

<p>The exact trajectory forms the butterfly attractor, the color gradient encodes time. The Carleman approximations (orange N=2, blue N=3) follow only briefly and then diverge (x marker).</p>

<h2 id="fixable-or-fundamental">Fixable or fundamental</h2>

<p>Still, the breakdown of the truncated Carleman matrix is not the final verdict on the quantum computer. By now there are workarounds such as pivot switching by Endo and Takahashi<sup id="fnref:endo2024"><a href="#fn:endo2024" class="footnote" rel="footnote" role="doc-noteref">5</a></sup> or pivot-shifted Carleman linearization<sup id="fnref:wang2025"><a href="#fn:wang2025" class="footnote" rel="footnote" role="doc-noteref">6</a></sup>: you shift and rescale the system around a pivot state so that the truncated dynamics no longer inevitably diverges. The exploding matrix is primarily a numerical problem. For regular dynamics it can be defused.</p>

<p>The hard limit sits elsewhere. Lewis and colleagues proved in 2024 that any quantum algorithm that exactly propagates a system with a positive Lyapunov exponent has a complexity that grows at least exponentially with the simulated time<sup id="fnref:lewis2024"><a href="#fn:lewis2024" class="footnote" rel="footnote" role="doc-noteref">7</a></sup>. That limit comes from the chaos itself; the Carleman truncation just happens to run into it first. Better code, pivot switching, or more qubits change nothing about it.</p>

<p>For the sci-fi plot this means keeping two things apart. The numerical breakdown of the Carleman method is a bug that clever people can fix. The exponential cost caused by chaos is a structural limit of the mathematics, not of the hardware.</p>

<h2 id="a-different-line-quantum-reservoir-computing">A different line: quantum reservoir computing</h2>

<p>The Carleman line tries to solve the differential equation itself. Since around 2023 a second route has established itself that reframes the problem. Quantum reservoir computing (QRC) uses the quantum system as a nonlinear substrate that approximates the attractor from a short observed time series. Only a classical readout layer is trained. The results are remarkable: Ahmed, Tennie and Magri predict extreme events in a turbulent shear flow model with correct timing using a recurrence-free variant<sup id="fnref:ahmed2024"><a href="#fn:ahmed2024" class="footnote" rel="footnote" role="doc-noteref">8</a></sup>, Steinegger and Räth reproduce the short-term behavior and long-term statistics of eight prototypical chaotic systems with four qubits<sup id="fnref:steinegger2025"><a href="#fn:steinegger2025" class="footnote" rel="footnote" role="doc-noteref">9</a></sup>, and Connerty and colleagues ran a quantum echo state network on real IBM hardware, far longer than the coherence times of the chip<sup id="fnref:connerty2025"><a href="#fn:connerty2025" class="footnote" rel="footnote" role="doc-noteref">10</a></sup>.</p>

<p>What matters is what QRC cannot do. It approximates the attractor and delivers short-term forecasts, not a single trajectory over long horizons. The Lyapunov exponent strikes here just the same, without Carleman and without truncation. QRC shows that quantum computers can indeed take over a certain prediction task. It is just not the one the novel promises: the exact future from complete initial data.</p>

<h2 id="getting-the-data-in-and-out">Getting the data in and out</h2>

<p>The half-sentence “once it has all the data” hides an assumption that is easy to miss: that the data can get into the machine at all. Algorithms of the HHL family deliver their exponential speedup only if the input is already available as a quantum state. Loading trillions of classical sensor readings into the amplitudes of a quantum register is the state preparation problem, and the standard answer to it, a quantum random access memory (QRAM)<sup id="fnref:giovannetti2008"><a href="#fn:giovannetti2008" class="footnote" rel="footnote" role="doc-noteref">11</a></sup>, exists essentially on paper: the circuits it requires are deep and error-prone, and nobody has built one at scale. Aaronson pointed at exactly this fine print years ago: if loading the data costs as much as solving the problem classically, the quantum advantage evaporates<sup id="fnref:aaronson2015"><a href="#fn:aaronson2015" class="footnote" rel="footnote" role="doc-noteref">12</a></sup>. So the sci-fi computer does not fail at the computation first, it fails at the loading dock, and by the time the state is prepared, the weather has moved on.</p>

<p>The other end is not free either. If the algorithm produces a quantum state that encodes the solution field, a measurement collapses it and yields only a limited amount of classical information per run. For an expectation value that is fine, for a complete weather field it is not. To be fair, the QRC line has learned to work around the collapse where it hurts most: weak measurement protocols read the reservoir gently enough that the computation can keep running<sup id="fnref:mujal2023"><a href="#fn:mujal2023" class="footnote" rel="footnote" role="doc-noteref">13</a></sup>, and feedback-driven QRC performs projective measurements but feeds the results straight back into the circuit as new rotation angles, keeping the memory of the system alive<sup id="fnref:kobayashi2024"><a href="#fn:kobayashi2024" class="footnote" rel="footnote" role="doc-noteref">14</a></sup>. Both tricks keep a running forecast going. Neither hands you the full solution vector of a differential equation.</p>

<p>And then there is the initial data itself. Chaos means sensitive dependence on initial conditions: \(\delta(t) \approx \delta(0)\,e^{\lambda t}\) with a positive Lyapunov exponent. To predict \(T\) time steps ahead you need roughly \(\lambda T\) additional bits of precision in the input. This holds for every computer, classical or quantum. “Having all the data” does not physically exist.</p>

<p>This is exactly the premise Laplace’s demon already hangs on. The demon could own perfect instruments and still fail: the required precision grows without bound with the forecast horizon, so “all the data” would have to mean arbitrarily many decimal places. The premise breaks classically, no quantum mechanics required. With that, the foundation of “once it has all the data” is gone, with or without a quantum computer.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Back to the original question: are there quantum algorithms for chaotic systems, and where does the novel’s idea break? They exist, and they are an active field of research, with Carleman linearization as the common thread. But the gap between “can efficiently simulate other quantum systems” and “predicts the weather once it has all the data” does not hang on the hardware. It hangs on two mathematical facts: linear time evolution meets nonlinear chaos, and the initial conditions would require a precision that does not physically exist. Both are structural, and structure does not yield to better hardware. On top of them sit the practical barriers at both ends, loading the data without a QRAM and reading the solution out through measurements: those are engineering, but engineering nobody has done.</p>

<p>That is also what makes the novel’s idea interesting: it points at real research, but the half-sentence “once it has all the data” quietly defines both problems away. The experiment above makes the first one visible, the second sits in a single exponential function. An afternoon of code is enough to make a whole novel’s premise wobble.</p>

<h2 id="sources">Sources</h2>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:liu2021">
      <p>Liu, Kolden, Krovi, Loureiro, Trivisa, Childs: <em>Efficient quantum algorithm for dissipative nonlinear differential equations</em>, <a href="https://www.pnas.org/doi/10.1073/pnas.2026805118">PNAS 118, e2026805118 (2021)</a>. <a href="#fnref:liu2021" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:tennie2025">
      <p>Tennie, Laizet, Lloyd, Magri: <em>Quantum computing for nonlinear differential equations and turbulence</em>, Nature Reviews Physics 7, 220 (2025). <a href="#fnref:tennie2025" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:gonzalez2025">
      <p>Gonzalez-Conde, Lewis, Bharadwaj, Sanz: <em>Quantum Carleman linearization efficiency in nonlinear fluid dynamics</em>, Phys. Rev. Research 7, 023254 (2025). <a href="https://arxiv.org/abs/2410.23057">arXiv:2410.23057</a> <a href="#fnref:gonzalez2025" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:sanavio2024">
      <p>Sanavio, Succi: <em>Lattice Boltzmann-Carleman quantum algorithm and simulation of the 1D Burgers equation at moderate Reynolds number</em>, AVS Quantum Science 6 (2024). <a href="#fnref:sanavio2024" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:endo2024">
      <p>Endo, Takahashi: <em>Divergence-free algorithms for solving nonlinear differential equations on quantum computers</em>, <a href="https://arxiv.org/abs/2411.16233">arXiv:2411.16233</a> (2024). <a href="#fnref:endo2024" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:wang2025">
      <p>Wang et al.: <em>Quantum Algorithms for Nonlinear Differential Equations via Pivot-Shifted Carleman Linearization</em>, <a href="https://arxiv.org/abs/2605.20071">arXiv:2605.20071</a> (2025). <a href="#fnref:wang2025" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:lewis2024">
      <p>Lewis, Eidenbenz, Nadiga, Subaşı: <em>Limitations for Quantum Algorithms to Solve Turbulent and Chaotic Systems</em>, <a href="https://quantum-journal.org/papers/q-2024-10-24-1509/">Quantum 8, 1509 (2024)</a>. <a href="#fnref:lewis2024" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:ahmed2024">
      <p>Ahmed, Tennie, Magri: <em>Prediction of chaotic dynamics and extreme events: A recurrence-free quantum reservoir computing approach</em>, Phys. Rev. Research 6, 043082 (2024). <a href="https://arxiv.org/abs/2405.03390">arXiv:2405.03390</a> <a href="#fnref:ahmed2024" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:steinegger2025">
      <p>Steinegger, Räth: <em>Predicting three-dimensional chaotic systems with four qubit quantum systems</em>, <a href="https://arxiv.org/abs/2501.15191">arXiv:2501.15191</a> (2025). <a href="#fnref:steinegger2025" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:connerty2025">
      <p>Connerty, Evans, Angelatos, Narayanan: <em>Quantum Observers: A NISQ Hardware Demonstration of Chaotic State Prediction Using Quantum Echo-state Networks</em>, <a href="https://arxiv.org/abs/2505.06799">arXiv:2505.06799</a> (2025). <a href="#fnref:connerty2025" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:giovannetti2008">
      <p>Giovannetti, Lloyd, Maccone: <em>Quantum Random Access Memory</em>, Phys. Rev. Lett. 100, 160501 (2008). <a href="https://arxiv.org/abs/0708.1879">arXiv:0708.1879</a> <a href="#fnref:giovannetti2008" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:aaronson2015">
      <p>Aaronson: <em>Read the fine print</em>, Nature Physics 11, 291 (2015). <a href="https://www.scottaaronson.com/papers/qml.pdf">PDF</a> <a href="#fnref:aaronson2015" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:mujal2023">
      <p>Mujal, Martínez-Peña, Giorgi, Soriano, Zambrini: <em>Time-series quantum reservoir computing with weak and projective measurements</em>, <a href="https://www.nature.com/articles/s41534-023-00682-z">npj Quantum Information 9, 16 (2023)</a>. <a href="#fnref:mujal2023" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:kobayashi2024">
      <p>Kobayashi, Fujii, Yamamoto: <em>Feedback-driven quantum reservoir computing for time-series analysis</em>, <a href="https://arxiv.org/abs/2406.15783">arXiv:2406.15783</a> (2024). <a href="#fnref:kobayashi2024" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><category term="blog" /><category term="quantum-computing" /><category term="physics" /><category term="numerics" /><category term="python" /><summary type="html"><![CDATA[I recently read Der Zorn des Oktopus (“The Wrath of the Octopus”) by Dirk Rossmann and Ralf Hoppe, a novel in which a quantum computer is built to predict chaotic systems. The basic idea: determine weather events once it has “all the data”. If x is supposed to happen, a causal chain y has to occur first, and a sufficiently large quantum computer simply plays that chain forward. In the novel the machine is promptly abused to influence events: what has to happen so that event X takes place?]]></summary></entry><entry><title type="html">Algorithms Belong in the Slice, Not the Container</title><link href="https://lmilz.dev/blog/2026/06/14/Algorithms-Belong-in-the-Slice-Not-the-Container.html" rel="alternate" type="text/html" title="Algorithms Belong in the Slice, Not the Container" /><published>2026-06-14T00:00:00+00:00</published><updated>2026-06-14T00:00:00+00:00</updated><id>https://lmilz.dev/blog/2026/06/14/Algorithms-Belong-in-the-Slice-Not-the-Container</id><content type="html" xml:base="https://lmilz.dev/blog/2026/06/14/Algorithms-Belong-in-the-Slice-Not-the-Container.html"><![CDATA[<p>I have spent the last few weeks building a small mini-STL in C++. This is a learning project: the point is not a library that competes with <code class="language-plaintext highlighter-rouge">std</code>, but understanding the design decisions behind containers, views and algorithms by building them myself. I am a big fan of the method-chaining style: <code class="language-plaintext highlighter-rouge">my_array.sort().find(5)</code>. That raised an interesting question: <em>where do the algorithms live?</em> In the C++ STL they are offered as free functions over pairs of iterators. That lets you implement an algorithm once and apply it to any range. Rust takes a different route with slices. My question was: how does the slice concept carry over to C++?</p>

<p>The answer sits in a 76-line class:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">template</span> <span class="o">&lt;</span><span class="k">typename</span> <span class="nc">T</span><span class="p">&gt;</span>
<span class="k">class</span> <span class="nc">Slice</span> <span class="p">{</span>
  <span class="nl">public:</span>
    <span class="k">constexpr</span> <span class="n">Slice</span><span class="p">()</span> <span class="k">noexcept</span> <span class="o">=</span> <span class="k">default</span><span class="p">;</span>
    <span class="k">constexpr</span> <span class="n">Slice</span><span class="p">(</span><span class="n">value_type</span><span class="o">*</span> <span class="n">data</span><span class="p">,</span> <span class="n">size_type</span> <span class="n">len</span><span class="p">)</span> <span class="k">noexcept</span>
        <span class="o">:</span> <span class="n">data_</span><span class="p">(</span><span class="n">data</span><span class="p">),</span> <span class="n">len_</span><span class="p">(</span><span class="n">len</span><span class="p">)</span> <span class="p">{}</span>

    <span class="p">[[</span><span class="n">nodiscard</span><span class="p">]]</span> <span class="k">constexpr</span> <span class="n">size_type</span> <span class="n">len</span><span class="p">()</span> <span class="k">const</span> <span class="k">noexcept</span> <span class="p">{</span> <span class="k">return</span> <span class="n">len_</span><span class="p">;</span> <span class="p">}</span>
    <span class="k">constexpr</span> <span class="n">value_type</span><span class="o">*</span> <span class="n">begin</span><span class="p">()</span> <span class="k">noexcept</span> <span class="p">{</span> <span class="k">return</span> <span class="n">data_</span><span class="p">;</span> <span class="p">}</span>
    <span class="k">constexpr</span> <span class="n">value_type</span><span class="o">*</span> <span class="n">end</span><span class="p">()</span> <span class="k">noexcept</span> <span class="p">{</span> <span class="k">return</span> <span class="n">data_</span> <span class="o">+</span> <span class="n">len_</span><span class="p">;</span> <span class="p">}</span>
    <span class="k">constexpr</span> <span class="n">value_type</span><span class="o">&amp;</span> <span class="k">operator</span><span class="p">[](</span><span class="n">size_type</span> <span class="n">index</span><span class="p">)</span> <span class="k">noexcept</span> <span class="p">{</span> <span class="k">return</span> <span class="n">data_</span><span class="p">[</span><span class="n">index</span><span class="p">];</span> <span class="p">}</span>

    <span class="p">[[</span><span class="n">nodiscard</span><span class="p">]]</span> <span class="k">constexpr</span> <span class="n">SortedSlice</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="n">sort</span><span class="p">()</span> <span class="p">{</span>
        <span class="c1">// Insertion sort, in place ...</span>
        <span class="k">return</span> <span class="n">SortedSlice</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">(</span><span class="n">data_</span><span class="p">,</span> <span class="n">len_</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">constexpr</span> <span class="n">Option</span><span class="o">&lt;</span><span class="n">size_type</span><span class="o">&gt;</span> <span class="n">find</span><span class="p">(</span><span class="k">const</span> <span class="n">value_type</span><span class="o">&amp;</span> <span class="n">value</span><span class="p">)</span> <span class="k">const</span> <span class="k">noexcept</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
    <span class="k">constexpr</span> <span class="kt">bool</span> <span class="n">contains</span><span class="p">(</span><span class="k">const</span> <span class="n">value_type</span><span class="o">&amp;</span> <span class="n">value</span><span class="p">)</span> <span class="k">const</span> <span class="k">noexcept</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>

<p>A <code class="language-plaintext highlighter-rouge">Slice&lt;T&gt;</code> owns nothing. It is nothing more than a pair of pointer and length. But this is where all the algorithms live. Not in the containers that hold the data. Exactly one place.</p>

<p>The <code class="language-plaintext highlighter-rouge">static_array</code> simply hands out its internal storage as a slice:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">constexpr</span> <span class="n">Slice</span><span class="o">&lt;</span><span class="k">const</span> <span class="n">T</span><span class="o">&gt;</span> <span class="n">as_slice</span><span class="p">()</span> <span class="k">const</span> <span class="k">noexcept</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">Slice</span><span class="o">&lt;</span><span class="k">const</span> <span class="n">T</span><span class="o">&gt;</span><span class="p">(</span><span class="n">data_</span><span class="p">,</span> <span class="n">N</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>That is Rust’s <code class="language-plaintext highlighter-rouge">&amp;[T]</code> deref coercion in C++ syntax. The consequence: <code class="language-plaintext highlighter-rouge">contains</code>, <code class="language-plaintext highlighter-rouge">find</code> and <code class="language-plaintext highlighter-rouge">sort</code> are written once and usable by any container that can hand out a slice. Add another contiguous container later and it gets search and sort for free, no algorithm code duplicated. <code class="language-plaintext highlighter-rouge">static_array::sort()</code> itself is therefore only three lines:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">template</span> <span class="o">&lt;</span><span class="k">typename</span> <span class="nc">T</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="kt">size_t</span> <span class="n">N</span><span class="p">&gt;</span>
<span class="k">constexpr</span> <span class="n">SortedArray</span><span class="o">&lt;</span><span class="n">T</span><span class="p">,</span> <span class="n">N</span><span class="o">&gt;</span> <span class="n">static_array</span><span class="o">&lt;</span><span class="n">T</span><span class="p">,</span> <span class="n">N</span><span class="o">&gt;::</span><span class="n">sort</span><span class="p">()</span> <span class="k">const</span> <span class="p">{</span>
    <span class="k">auto</span> <span class="n">copy</span> <span class="o">=</span> <span class="o">*</span><span class="k">this</span><span class="p">;</span>
    <span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="p">(</span><span class="n">copy</span><span class="p">.</span><span class="n">as_mut_slice</span><span class="p">().</span><span class="n">sort</span><span class="p">());</span>  <span class="c1">// the slice does the work</span>
    <span class="k">return</span> <span class="n">SortedArray</span><span class="o">&lt;</span><span class="n">T</span><span class="p">,</span> <span class="n">N</span><span class="o">&gt;</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">move</span><span class="p">(</span><span class="n">copy</span><span class="p">));</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The container makes a copy, hands out a mutable slice over it, and delegates. No sorting algorithm in the container itself. The STL solves this differently: algorithms in <code class="language-plaintext highlighter-rouge">&lt;algorithm&gt;</code> as free functions over iterator pairs. A different abstraction with a different trade-off, the length is not bundled in, it follows from <code class="language-plaintext highlighter-rouge">end - begin</code>. When you build it yourself you have to commit to <em>one</em> variant and feel in your own code what it costs.</p>

<p>The obvious follow-up: which containers does this even work for? It hangs on a single property, is the memory contiguous or not. From that a clear split follows.</p>

<p>Slice heirs are all containers that allocate contiguously: a fixed-size or growable array, the dense matrices and vectors of a linear-algebra type, the slots of an open-addressing hash table. Whatever the structure, as long as its elements sit back to back in memory it can hand out a slice and inherits every algorithm for free.</p>

<p>Adapters like <code class="language-plaintext highlighter-rouge">stack</code> and <code class="language-plaintext highlighter-rouge">queue</code> build on a container and deliberately restrict the interface. They do not hand out a slice, because <code class="language-plaintext highlighter-rouge">push</code>, <code class="language-plaintext highlighter-rouge">pop</code> and <code class="language-plaintext highlighter-rouge">top</code> make sense precisely when the container is <em>not</em> sortable and not freely indexable. A stack that hands out a slice is no longer a stack. This is the point where the rule “write once, use everywhere” deliberately stops.</p>

<p>Iterator containers are the structures whose memory is not contiguous: linked lists, trees without an array backing, graphs with adjacency lists, hash maps with separate chaining. Here a fat pointer is pointless, because it would have to point at several scattered regions. Instead of a slice an iterator steps in, closing off the layer downward cleanly. Rust shows the same convention: contiguous containers implement <code class="language-plaintext highlighter-rouge">Deref&lt;Target=[T]&gt;</code> and thereby hand out <code class="language-plaintext highlighter-rouge">&amp;[T]</code>, pointer-backed structures only offer an <code class="language-plaintext highlighter-rouge">Iterator</code>. The two are not mutually exclusive: <code class="language-plaintext highlighter-rouge">Vec&lt;T&gt;</code> has both, because its iterator is internally the same pointer arithmetic over the same memory.</p>

<p>From that follows the question I ask myself for every new container: array-backed or pointer-backed? Array-backed means slice for free, cache-friendly, with size limits. Pointer-backed means iterator API, more flexible. Both are legitimate, mixing them is the trap: a non-contiguous container that hands out a slice lies about its memory layout. This question has become the first one I ask, before I even start writing a new container.</p>

<p>What surprised me most along the way is how far the principle stretches. <code class="language-plaintext highlighter-rouge">static_array::sort()</code> returns no sorted <code class="language-plaintext highlighter-rouge">static_array</code>, but a new type:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[[</span><span class="n">nodiscard</span><span class="p">]]</span> <span class="k">constexpr</span> <span class="n">SortedArray</span><span class="o">&lt;</span><span class="n">T</span><span class="p">,</span> <span class="n">N</span><span class="o">&gt;</span> <span class="n">sort</span><span class="p">()</span> <span class="k">const</span><span class="p">;</span>
</code></pre></div></div>

<p>And <code class="language-plaintext highlighter-rouge">binary_search</code> is available only on that type:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">template</span> <span class="o">&lt;</span><span class="k">typename</span> <span class="nc">T</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="kt">size_t</span> <span class="n">N</span><span class="p">&gt;</span>
<span class="k">class</span> <span class="nc">SortedArray</span> <span class="p">{</span>
  <span class="nl">public:</span>
    <span class="k">constexpr</span> <span class="n">BinarySearchResult</span> <span class="n">binary_search</span><span class="p">(</span><span class="k">const</span> <span class="n">T</span><span class="o">&amp;</span> <span class="n">value</span><span class="p">)</span> <span class="k">const</span> <span class="k">noexcept</span><span class="p">;</span>
    <span class="c1">// sort() does not exist here anymore</span>
<span class="p">};</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">binary_search</code> on an unsorted array silently violates the precondition of the algorithm: no compiler error, no crash, just a wrong result. The STL solves this by convention: you just sort beforehand, and the compiler does not help. In my implementation <code class="language-plaintext highlighter-rouge">binary_search</code> is simply not a member of <code class="language-plaintext highlighter-rouge">static_array</code>. It is only available on <code class="language-plaintext highlighter-rouge">SortedArray</code>:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">arr</span><span class="p">.</span><span class="n">binary_search</span><span class="p">(</span><span class="mi">42</span><span class="p">);</span>          <span class="c1">// compile error: no member</span>
<span class="n">arr</span><span class="p">.</span><span class="n">sort</span><span class="p">().</span><span class="n">binary_search</span><span class="p">(</span><span class="mi">42</span><span class="p">);</span>   <span class="c1">// OK</span>
</code></pre></div></div>

<p>That is exactly the point where the type system makes an assumption of the program logic <em>visible</em>. Rust’s <code class="language-plaintext highlighter-rouge">[T]::binary_search()</code> shows the alternative: the method exists on every slice, the precondition lies with the caller, the compiler does not check it. The newtype approach, which enforces the same contract at the type level, you find in Rust as a deliberate design pattern, not as a stdlib feature. Building it yourself, you grasp <em>why</em> that is so. Using the STL, it stays a good tip from a book.</p>

<p>Both observations are ultimately one: build the right abstraction layer, and the algorithms come for free. Not because my 76-line Slice class is better than <code class="language-plaintext highlighter-rouge">std::span</code>. But because at every line you make a design decision that stays invisible in the STL. Building the slice myself was the fastest way not just to know Rust’s <code class="language-plaintext highlighter-rouge">&amp;[T]</code> deref coercion, but to understand why it exists.</p>]]></content><author><name></name></author><category term="blog" /><category term="c++" /><category term="rust" /><category term="software-engineering" /><category term="learning-by-doing" /><summary type="html"><![CDATA[I have spent the last few weeks building a small mini-STL in C++. This is a learning project: the point is not a library that competes with std, but understanding the design decisions behind containers, views and algorithms by building them myself. I am a big fan of the method-chaining style: my_array.sort().find(5). That raised an interesting question: where do the algorithms live? In the C++ STL they are offered as free functions over pairs of iterators. That lets you implement an algorithm once and apply it to any range. Rust takes a different route with slices. My question was: how does the slice concept carry over to C++?]]></summary></entry><entry><title type="html">Synapse: A Knowledge Graph for the Whole Monorepo</title><link href="https://lmilz.dev/blog/2026/06/01/Synapse-A-Knowledge-Graph-for-the-Whole-Monorepo.html" rel="alternate" type="text/html" title="Synapse: A Knowledge Graph for the Whole Monorepo" /><published>2026-06-01T00:00:00+00:00</published><updated>2026-06-01T00:00:00+00:00</updated><id>https://lmilz.dev/blog/2026/06/01/Synapse-A-Knowledge-Graph-for-the-Whole-Monorepo</id><content type="html" xml:base="https://lmilz.dev/blog/2026/06/01/Synapse-A-Knowledge-Graph-for-the-Whole-Monorepo.html"><![CDATA[<p>Everything in my second brain lives in one repository: Zettelkasten notes, a compiled wiki, journal entries, C++ projects, Rust libraries, Bazel build files, blog drafts. A thought and the code that proves it belong in the same place, <a href="/blog/2026/05/18/Code-is-a-Thinking-Tool-Not-an-Artifact.html">and I have written about that before</a>.</p>

<p>Obsidian, which I use to navigate the notes, only sees the Markdown half. A note about cache line alignment links to a wiki page on CPU architecture, which touches the same topic as a C++ file that implements a memory pool, which is built by a <code class="language-plaintext highlighter-rouge">BUILD</code> file that depends on a benchmark library. Obsidian sees two of those connections. The rest are invisible.</p>

<p>I built Synapse to make the whole graph visible. It crawls a repository, extracts references from every file type it understands, resolves them to concrete paths, and serves a force-directed graph over a small HTTP API. No database, no external Go dependencies. D3.js is embedded in the binary; the graph view works offline without any CDN.</p>

<h2 id="what-gets-indexed">What Gets Indexed</h2>

<p>The core question for a tool like this is: what counts as a “reference”?</p>

<p>For Markdown files, there are two kinds: standard links <code class="language-plaintext highlighter-rouge">[text](path)</code> and Obsidian-style wiki-links <code class="language-plaintext highlighter-rouge">[[note name]]</code>. For C and C++ files, local <code class="language-plaintext highlighter-rouge">#include "header.h"</code> directives. For Rust, <code class="language-plaintext highlighter-rouge">use crate::module</code> and <code class="language-plaintext highlighter-rouge">mod submodule</code> declarations. For Bazel <code class="language-plaintext highlighter-rouge">BUILD</code> files, the <code class="language-plaintext highlighter-rouge">deps</code>, <code class="language-plaintext highlighter-rouge">srcs</code>, <code class="language-plaintext highlighter-rouge">data</code>, and similar attributes.</p>

<p>The extractor design reflects this. Each file type gets one or more <code class="language-plaintext highlighter-rouge">Extractor</code> implementations:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">type</span> <span class="n">Extractor</span> <span class="k">interface</span> <span class="p">{</span>
    <span class="n">Extract</span><span class="p">(</span><span class="n">content</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">[]</span><span class="n">RawRef</span>
<span class="p">}</span>
</code></pre></div></div>

<p>A <code class="language-plaintext highlighter-rouge">RawRef</code> is the literal text from the source file, before any path resolution. <code class="language-plaintext highlighter-rouge">[[some-note]]</code> in Markdown becomes <code class="language-plaintext highlighter-rouge">RawRef{Target: "some-note", Type: EdgeWikiLink}</code>. <code class="language-plaintext highlighter-rouge">#include "util/math.h"</code> becomes <code class="language-plaintext highlighter-rouge">RawRef{Target: "util/math.h", Type: EdgeCInclude}</code>. The extractor does not know where the file lives; it only reads bytes and returns what it finds.</p>

<p>The dispatch is by extension for most types, and by exact filename for cases where the extension alone is not enough:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">var</span> <span class="n">ByExtension</span> <span class="o">=</span> <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">][]</span><span class="n">Extractor</span><span class="p">{</span>
    <span class="s">"md"</span><span class="o">:</span>  <span class="p">{</span><span class="n">markdownLinkExtractor</span><span class="p">{},</span> <span class="n">wikilinkExtractor</span><span class="p">{}},</span>
    <span class="s">"cpp"</span><span class="o">:</span> <span class="p">{</span><span class="n">cIncludeExtractor</span><span class="p">{}},</span>
    <span class="s">"rs"</span><span class="o">:</span>  <span class="p">{</span><span class="n">rustUseExtractor</span><span class="p">{},</span> <span class="n">rustModExtractor</span><span class="p">{}},</span>
    <span class="s">"bzl"</span><span class="o">:</span> <span class="p">{</span><span class="n">bazelDepExtractor</span><span class="p">{}},</span>
    <span class="c">// ...</span>
<span class="p">}</span>

<span class="c">// Extension-less files need separate handling.</span>
<span class="k">var</span> <span class="n">ByName</span> <span class="o">=</span> <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">][]</span><span class="n">Extractor</span><span class="p">{</span>
    <span class="s">"BUILD"</span><span class="o">:</span>     <span class="p">{</span><span class="n">bazelDepExtractor</span><span class="p">{}},</span>
    <span class="s">"WORKSPACE"</span><span class="o">:</span> <span class="p">{</span><span class="n">bazelDepExtractor</span><span class="p">{}},</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The worker pool reads each file and dispatches to both maps:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">ex</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">extract</span><span class="o">.</span><span class="n">ByExtension</span><span class="p">[</span><span class="n">ext</span><span class="p">]</span> <span class="p">{</span>
    <span class="n">refs</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="n">refs</span><span class="p">,</span> <span class="n">ex</span><span class="o">.</span><span class="n">Extract</span><span class="p">(</span><span class="n">content</span><span class="p">)</span><span class="o">...</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">ex</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">extract</span><span class="o">.</span><span class="n">ByName</span><span class="p">[</span><span class="n">filepath</span><span class="o">.</span><span class="n">Base</span><span class="p">(</span><span class="n">path</span><span class="p">)]</span> <span class="p">{</span>
    <span class="n">refs</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="n">refs</span><span class="p">,</span> <span class="n">ex</span><span class="o">.</span><span class="n">Extract</span><span class="p">(</span><span class="n">content</span><span class="p">)</span><span class="o">...</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This turned out to be necessary because <code class="language-plaintext highlighter-rouge">BUILD</code> files have no extension, and a lookup in <code class="language-plaintext highlighter-rouge">ByExtension[""]</code> would match any extension-less file in the repository.</p>

<h2 id="no-graph-database">No Graph Database</h2>

<p>The graph itself is a plain in-memory structure. Two maps: paths to node pointers, edge keys to edge pointers. A sync.RWMutex for concurrent ingestion.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">type</span> <span class="n">Node</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">Path</span>     <span class="kt">string</span>   <span class="s">`json:"path"`</span>
    <span class="n">FileType</span> <span class="kt">string</span>   <span class="s">`json:"file_type"`</span>
    <span class="n">Tags</span>     <span class="p">[]</span><span class="kt">string</span> <span class="s">`json:"tags,omitempty"`</span>
    <span class="n">Outgoing</span> <span class="p">[]</span><span class="n">Edge</span>   <span class="s">`json:"outgoing"`</span>
    <span class="n">Incoming</span> <span class="p">[]</span><span class="n">Edge</span>   <span class="s">`json:"incoming"`</span>
<span class="p">}</span>

<span class="k">type</span> <span class="n">BrainGraph</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">nodes</span> <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="o">*</span><span class="n">Node</span>
    <span class="n">edges</span> <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="o">*</span><span class="n">Edge</span>
    <span class="n">mu</span>    <span class="n">sync</span><span class="o">.</span><span class="n">RWMutex</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Each node holds its own adjacency lists. After the crawl, the graph is read-only and the API serves from it directly. For a repository of 1,500 files, this fits comfortably in a few megabytes.</p>

<p>The crawl itself uses a worker pool. Walking the file tree and reading files is I/O bound; multiple goroutines keep the disk busy while others wait. The workers return <code class="language-plaintext highlighter-rouge">fileResult</code> structs over a channel; the main goroutine collects results and adds edges to the graph after all workers finish:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">jobs</span>    <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="k">chan</span> <span class="n">fileJob</span><span class="p">,</span>    <span class="nb">len</span><span class="p">(</span><span class="n">files</span><span class="p">))</span>
<span class="n">results</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="k">chan</span> <span class="n">fileResult</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">files</span><span class="p">))</span>

<span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">cfg</span><span class="o">.</span><span class="n">Workers</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span> <span class="p">{</span>
    <span class="n">wg</span><span class="o">.</span><span class="n">Add</span><span class="p">(</span><span class="m">1</span><span class="p">)</span>
    <span class="k">go</span> <span class="n">crawlWorker</span><span class="p">(</span><span class="n">cfg</span><span class="p">,</span> <span class="n">jobs</span><span class="p">,</span> <span class="n">results</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">wg</span><span class="p">)</span>
<span class="p">}</span>

<span class="c">// Collect after workers close results channel</span>
<span class="k">for</span> <span class="n">res</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">results</span> <span class="p">{</span>
    <span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">ref</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">res</span><span class="o">.</span><span class="n">refs</span> <span class="p">{</span>
        <span class="n">target</span><span class="p">,</span> <span class="n">resolved</span> <span class="o">:=</span> <span class="n">resolver</span><span class="o">.</span><span class="n">ResolveRawRef</span><span class="p">(</span><span class="n">ref</span><span class="p">,</span> <span class="n">res</span><span class="o">.</span><span class="n">path</span><span class="p">)</span>
        <span class="n">g</span><span class="o">.</span><span class="n">AddEdge</span><span class="p">(</span><span class="n">graph</span><span class="o">.</span><span class="n">Edge</span><span class="p">{</span> <span class="n">Source</span><span class="o">:</span> <span class="n">res</span><span class="o">.</span><span class="n">path</span><span class="p">,</span> <span class="n">Target</span><span class="o">:</span> <span class="n">target</span><span class="p">,</span> <span class="o">...</span> <span class="p">})</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Resolution happens centrally because it needs the full file tree. A wiki-link <code class="language-plaintext highlighter-rouge">[[Rust Ownership]]</code> needs to be matched against all indexed paths to find <code class="language-plaintext highlighter-rouge">zettelkasten/Rust Ownership.md</code>. The extractor only sees one file at a time and cannot do this.</p>

<h2 id="three-bugs-that-only-real-data-reveals">Three Bugs That Only Real Data Reveals</h2>

<p>I wrote tests for each extractor. They all passed. Then I ran the crawler against my actual repository and found three classes of errors that no unit test had caught.</p>

<p><strong>C++ attributes look like wiki-links.</strong> My Zettelkasten notes contain C++ code examples in fenced blocks. A note about <code class="language-plaintext highlighter-rouge">[[nodiscard]]</code> as a design technique wrote it in a code block. The extractor ran the wiki-link regex over the raw bytes and added 113 edges to nonexistent nodes: <code class="language-plaintext highlighter-rouge">[[nodiscard]]</code>, <code class="language-plaintext highlighter-rouge">[[likely]]</code>, <code class="language-plaintext highlighter-rouge">[[unlikely]]</code>, and one memorable <code class="language-plaintext highlighter-rouge">[[gnu::target_clones("sse4.2,avx2,avx512f,default")]]</code>.</p>

<p>The fix was to strip code regions before running the regex, while preserving all newline characters so that byte offsets for line numbers stayed valid:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">stripCode</span><span class="p">(</span><span class="n">src</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">[]</span><span class="kt">byte</span> <span class="p">{</span>
    <span class="n">dst</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">src</span><span class="p">))</span>
    <span class="nb">copy</span><span class="p">(</span><span class="n">dst</span><span class="p">,</span> <span class="n">src</span><span class="p">)</span>
    <span class="c">// Replace fenced blocks and inline spans with spaces.</span>
    <span class="c">// Newlines are never touched, so line counts stay correct.</span>
    <span class="o">...</span>
    <span class="k">return</span> <span class="n">dst</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong><code class="language-plaintext highlighter-rouge">filepath.Ext</code> has an opinion about book titles.</strong> The wiki-link resolver builds a lookup table of <code class="language-plaintext highlighter-rouge">lowercase-basename-without-extension -&gt; file path</code>. A note titled <code class="language-plaintext highlighter-rouge">Robert C. Martin - Clean Code.md</code> should be resolvable by <code class="language-plaintext highlighter-rouge">[[Robert C. Martin - Clean Code]]</code>. It was not.</p>

<p><code class="language-plaintext highlighter-rouge">filepath.Ext("Robert C. Martin - Clean Code")</code> returns <code class="language-plaintext highlighter-rouge">. Martin - Clean Code</code>, because “the extension is the suffix beginning at the last dot in the last element of the path.” The last dot in that title is the one after <code class="language-plaintext highlighter-rouge">C</code>. The lookup key computed from the link target was <code class="language-plaintext highlighter-rouge">"robert c"</code>, not <code class="language-plaintext highlighter-rouge">"robert c. martin - clean code"</code>.</p>

<p>The fix: try the full lowercased basename as a key first, fall back to extension-stripping only if that fails. This handles both note titles with dots and explicit <code class="language-plaintext highlighter-rouge">[[note.md]]</code>-style references.</p>

<p><strong>Bazel labels have colons.</strong> The Bazel extractor used a regex that excluded colons from label strings: <code class="language-plaintext highlighter-rouge">[^":]+</code>. Bazel’s label syntax is <code class="language-plaintext highlighter-rouge">//package:target</code>. Every cross-package dependency was silently dropped. The fix was a regex that matches the actual label grammar rather than “everything that is not a quote or colon.”</p>

<h2 id="tags-need-to-be-nodes">Tags Need to Be Nodes</h2>

<p><img src="/assets/images/Synapse_tags.png" alt="Synapse graph view with tag nodes enabled" /></p>

<p>The first version of the graph rendered 1,489 nodes with a standard D3 <code class="language-plaintext highlighter-rouge">forceSimulation</code>. The result was indistinguishable from a scatterplot. Every node drifted away from every other node with equal force. No clusters, no structure.</p>

<p>The Obsidian graph, visualizing the same notes, shows clear topic regions: a dense area for <code class="language-plaintext highlighter-rouge">#rust</code> notes, another for <code class="language-plaintext highlighter-rouge">#embedded</code>, a looser cloud for <code class="language-plaintext highlighter-rouge">#journal</code> entries. Obsidian treats tags as nodes.</p>

<p>Every note with <code class="language-plaintext highlighter-rouge">tags: [rust, embedded]</code> has two edges in Obsidian’s graph: one to a <code class="language-plaintext highlighter-rouge">rust</code> node, one to an <code class="language-plaintext highlighter-rouge">embedded</code> node. Notes sharing tags end up near each other because they are all connected to the same anchor. The clustering is an emergent property of the graph structure, not a layout algorithm.</p>

<p>Synapse extracts tags from YAML frontmatter for each Markdown file and includes them in the API response. The frontend creates virtual tag nodes from the frequency map and adds note-to-tag edges to the simulation. No tag data is stored server-side beyond the <code class="language-plaintext highlighter-rouge">tags</code> field on each node.</p>

<p>The other piece is <code class="language-plaintext highlighter-rouge">distanceMax</code> on <code class="language-plaintext highlighter-rouge">forceManyBody</code>. Without it, D3’s Barnes-Hut approximation treats distant clusters as combined bodies and generates a global outward pressure that blows every cluster apart. With <code class="language-plaintext highlighter-rouge">distanceMax(200)</code>, nodes only repel their local neighborhood and distant clusters do not interact at all. [A separate post covers the D3 physics in more detail.][d3-post]</p>

<p>Running against my repository: 1,082 of 1,134 Markdown files have tags, 240 unique tags, top tags are <code class="language-plaintext highlighter-rouge">#learning</code> (361 notes), <code class="language-plaintext highlighter-rouge">#journal</code> (176), <code class="language-plaintext highlighter-rouge">#software-engineering</code> (149), <code class="language-plaintext highlighter-rouge">#rust</code> (138), <code class="language-plaintext highlighter-rouge">#embedded</code> (123). The graph now shows these as distinct regions with overlap where topics intersect.</p>

<h2 id="what-it-shows-that-obsidian-does-not">What It Shows That Obsidian Does Not</h2>

<p>The most useful output is the stats.</p>

<p>My repository has 2,204 edges across 1,489 nodes. 699 of those edges point to files that do not exist: wiki-links to notes I referenced but have not written yet, C++ headers that are system includes, Rust crate references that resolve to external packages. These are knowingly unresolved and that is fine. But examining them surfaces planned notes, gaps in the knowledge base, and occasionally a broken link that used to point somewhere.</p>

<p>There are also 673 orphan nodes: files with no incoming or outgoing edges at all. Most are blog drafts and README files. Some are Zettelkasten notes that have never been linked from anywhere. Seeing them listed is useful: either they should be linked or they should not exist.</p>

<p>The top hub in my repository is <code class="language-plaintext highlighter-rouge">zettelkasten/Zettelkasten Methodik.md</code> with degree 87: 71 notes link to it, and it links to 16 others. The second is a wiki page on software architecture. These are the conceptual load-bearing nodes of the second brain, and they are not obvious when browsing the vault file by file.</p>

<p>The connected components count is less reassuring: 877 separate components, meaning the graph is nowhere close to a single connected whole. Most notes live in small clusters of 2 to 10 nodes with no path to the rest.</p>

<p><img src="/assets/images/Synapse_folder.png" alt="Synapse graph view with directory clustering" /></p>

<h2 id="running-it">Running It</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>go <span class="nb">install </span>codeberg.org/lmilz/synapse/cmd/synapse@latest
synapse serve /path/to/repo
</code></pre></div></div>

<p>The server starts on port 8080. The graph view loads the full index, builds tag nodes in the browser, and lets you filter by file type, minimum degree, or search. All data lives in the Go binary at runtime; there is no configuration file, no schema, no migration.</p>

<p>The source is at <a href="https://github.com/lmilz/synapse">githut.com/lmilz/synapse</a> and <a href="https://codeberg.org/lmilz/synapse">codeberg.org/lmilz/synapse</a>.</p>]]></content><author><name></name></author><category term="blog" /><category term="go" /><category term="knowledge-management" /><category term="second-brain" /><category term="graph" /><category term="d3js" /><summary type="html"><![CDATA[Everything in my second brain lives in one repository: Zettelkasten notes, a compiled wiki, journal entries, C++ projects, Rust libraries, Bazel build files, blog drafts. A thought and the code that proves it belong in the same place, and I have written about that before.]]></summary></entry><entry><title type="html">Code is a Thinking Tool, Not an Artifact</title><link href="https://lmilz.dev/blog/2026/05/18/Code-is-a-Thinking-Tool-Not-an-Artifact.html" rel="alternate" type="text/html" title="Code is a Thinking Tool, Not an Artifact" /><published>2026-05-18T00:00:00+00:00</published><updated>2026-05-18T00:00:00+00:00</updated><id>https://lmilz.dev/blog/2026/05/18/Code-is-a-Thinking-Tool-Not-an-Artifact</id><content type="html" xml:base="https://lmilz.dev/blog/2026/05/18/Code-is-a-Thinking-Tool-Not-an-Artifact.html"><![CDATA[<p>Since school I have had a problem with notes. Taking legible notes during a lecture was never possible for me, so I would rewrite everything neatly in the evening. Digital notes, a tablet with a stylus, OneNote: I tried a lot. But none of it ever truly convinced me.</p>

<p>When my second son was born I had a month off and read Tiago Forte’s book: <em>How to Build a Second Brain</em>. PARA and CODE clicked immediately. Finally a framework that made sense. But the longer I worked with it, the clearer the problem became:</p>

<p>All of these approaches treat knowledge as text.</p>

<p><strong>As a developer, my knowledge also lives in code.</strong></p>

<p>PARA structures by purpose, not topic: not “where does this belong?” but “which project needs it?” CODE keeps information moving from capture to expression. Information serves a purpose, or it stays out. Right foundation. But only for text.</p>

<h2 id="the-difference-between-describing-and-understanding">The Difference Between Describing and Understanding</h2>

<p>PARA answers: “What do I use this for?”
When developing, I am often more interested in: “What happens when I change this?”</p>

<p><strong>Code is not an artifact, it is a thinking tool.</strong></p>

<p>I have a note that says: use a tag type to distinguish iterators between containers. But six months later I could not reconstruct why.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Why not just: using iterator = T*?</span>
<span class="c1">// std::iterator_traits&lt;T*&gt; is specialized, all STL algorithms work.</span>
<span class="c1">// But then vector&lt;int&gt;::iterator and static_array&lt;int, 5&gt;::iterator</span>
<span class="c1">// are the same type — the compiler cannot catch misuse across containers.</span>
<span class="c1">//</span>
<span class="c1">// Fix: a Tag parameter. Empty struct, private to the container.</span>
<span class="c1">// Zero runtime cost, the compiler erases it entirely.</span>
<span class="c1">// Each container gets a distinct iterator type. Misuse becomes a compile error.</span>
<span class="k">template</span> <span class="o">&lt;</span><span class="k">typename</span> <span class="nc">T</span><span class="p">,</span> <span class="k">typename</span> <span class="nc">Tag</span><span class="p">,</span> <span class="kt">bool</span> <span class="n">IsConst</span><span class="p">&gt;</span>
<span class="k">class</span> <span class="nc">pointer_iterator</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">};</span>

<span class="c1">// Declared inside static_array — no outside type can share it:</span>
<span class="k">class</span> <span class="nc">static_array</span> <span class="p">{</span>
    <span class="k">struct</span> <span class="nc">iterator_tag</span> <span class="p">{};</span>
    <span class="k">using</span> <span class="n">iterator</span> <span class="o">=</span> <span class="n">pointer_iterator</span><span class="o">&lt;</span><span class="n">T</span><span class="p">,</span> <span class="n">iterator_tag</span><span class="p">,</span> <span class="nb">false</span><span class="o">&gt;</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>

<p>The comments are the note. The code is the proof. But that only works if the note and the code can actually live together.</p>

<h2 id="code-as-a-first-class-citizen">Code as a First-Class Citizen</h2>

<p>For a long time I had everything across separate repos: blog, journal, Zettelkasten, small projects. A note about a data structure and the corresponding code lived in different places. Today everything is in a monorepo. A thought and the code that proves it live in the same repository, often in the same commit.</p>

<p>Kepano, the CEO of Obsidian, called it “File over App.” Knowledge does not belong in a tool, it belongs in files you own and can read without anyone running a server. That applies to Markdown just as much as to code, with the difference that code can be executed.</p>

<h2 id="writing-and-seeing">Writing and Seeing</h2>

<p>My current setup uses two tools, not one. Neovim for writing: fast, close to the code, with the movements I have known for years. Obsidian for the graph view: I see which notes are connected, which are isolated, where clusters form. That overview I cannot get in Neovim.</p>

<p><img src="/assets/images/Obsidian_Graph.png" alt="Obsidian Vault Graph View" /></p>

<p>For a long time I thought I had to choose: Obsidian or Neovim. Every time the question was: which tool is the right one?</p>

<p>The question was wrong. As long as my notes are plain Markdown in a Git repo, I can use both tools in parallel. Neovim writes, Obsidian shows, both work on the same files. The repo is the source, the tools are views on it.</p>

<p>At the start of this post I wrote that for a long time I could not name why none of the earlier systems worked. Today I can: because they made the tool the source, not the file.</p>]]></content><author><name></name></author><category term="blog" /><category term="productivity" /><category term="second-brain" /><summary type="html"><![CDATA[Since school I have had a problem with notes. Taking legible notes during a lecture was never possible for me, so I would rewrite everything neatly in the evening. Digital notes, a tablet with a stylus, OneNote: I tried a lot. But none of it ever truly convinced me.]]></summary></entry><entry><title type="html">Wrapping a C library in RAII: unique_ptr with custom deleters and why Subsystem can’t be moveable</title><link href="https://lmilz.dev/blog/2026/05/15/Wrapping-a-C-library-in-RAII-unique_ptr-with-custom-deleters-and-why-Subsystem-canot-be-moveable.html" rel="alternate" type="text/html" title="Wrapping a C library in RAII: unique_ptr with custom deleters and why Subsystem can’t be moveable" /><published>2026-05-15T00:00:00+00:00</published><updated>2026-05-15T00:00:00+00:00</updated><id>https://lmilz.dev/blog/2026/05/15/Wrapping-a-C-library-in-RAII-unique_ptr-with-custom-deleters-and-why-Subsystem-canot-be-moveable</id><content type="html" xml:base="https://lmilz.dev/blog/2026/05/15/Wrapping-a-C-library-in-RAII-unique_ptr-with-custom-deleters-and-why-Subsystem-canot-be-moveable.html"><![CDATA[<p>I got into the habit of wrapping C libraries before I start working with them. Not out of principle, but because I have seen the same mistakes often enough.</p>

<p>C has no RAII. Every resource you create, you have to release yourself. One function returns a pointer, another takes it back to free it. Between those two calls sits all the code that can go wrong. That leads to three recurring bugs: leaks on early return, when a later call fails and the cleanup code is never reached; double-free, when a pointer gets copied by accident and destroyed twice; and unclear ownership, because the return type alone does not say who is responsible for cleanup.</p>

<p>SDL2 is a typical C library in this situation, and it serves as the concrete example here. <code class="language-plaintext highlighter-rouge">std::unique_ptr</code> with a custom deleter solves all three. The trick is connecting the SDL type to its cleanup function:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="nc">Deleter</span> <span class="p">{</span>
    <span class="kt">void</span> <span class="k">operator</span><span class="p">()(</span><span class="n">SDL_Window</span><span class="o">*</span>   <span class="n">p</span><span class="p">)</span> <span class="k">const</span> <span class="k">noexcept</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="n">p</span><span class="p">)</span> <span class="n">SDL_DestroyWindow</span><span class="p">(</span><span class="n">p</span><span class="p">);</span>   <span class="p">}</span>
    <span class="kt">void</span> <span class="nf">operator</span><span class="p">()(</span><span class="n">SDL_Renderer</span><span class="o">*</span> <span class="n">p</span><span class="p">)</span> <span class="k">const</span> <span class="k">noexcept</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="n">p</span><span class="p">)</span> <span class="n">SDL_DestroyRenderer</span><span class="p">(</span><span class="n">p</span><span class="p">);</span> <span class="p">}</span>
    <span class="kt">void</span> <span class="nf">operator</span><span class="p">()(</span><span class="n">SDL_Texture</span><span class="o">*</span>  <span class="n">p</span><span class="p">)</span> <span class="k">const</span> <span class="k">noexcept</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="n">p</span><span class="p">)</span> <span class="n">SDL_DestroyTexture</span><span class="p">(</span><span class="n">p</span><span class="p">);</span>  <span class="p">}</span>
    <span class="kt">void</span> <span class="nf">operator</span><span class="p">()(</span><span class="n">SDL_Surface</span><span class="o">*</span>  <span class="n">p</span><span class="p">)</span> <span class="k">const</span> <span class="k">noexcept</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="n">p</span><span class="p">)</span> <span class="n">SDL_FreeSurface</span><span class="p">(</span><span class="n">p</span><span class="p">);</span>     <span class="p">}</span>
<span class="p">};</span>

<span class="k">using</span> <span class="n">WindowPtr</span>   <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">unique_ptr</span><span class="o">&lt;</span><span class="n">SDL_Window</span><span class="p">,</span>   <span class="n">Deleter</span><span class="o">&gt;</span><span class="p">;</span>
<span class="k">using</span> <span class="n">RendererPtr</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">unique_ptr</span><span class="o">&lt;</span><span class="n">SDL_Renderer</span><span class="p">,</span> <span class="n">Deleter</span><span class="o">&gt;</span><span class="p">;</span>
<span class="k">using</span> <span class="n">TexturePtr</span>  <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">unique_ptr</span><span class="o">&lt;</span><span class="n">SDL_Texture</span><span class="p">,</span>  <span class="n">Deleter</span><span class="o">&gt;</span><span class="p">;</span>
<span class="k">using</span> <span class="n">SurfacePtr</span>  <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">unique_ptr</span><span class="o">&lt;</span><span class="n">SDL_Surface</span><span class="p">,</span>  <span class="n">Deleter</span><span class="o">&gt;</span><span class="p">;</span>
</code></pre></div></div>

<p>One type, four resources. <code class="language-plaintext highlighter-rouge">unique_ptr</code> picks the right overload based on the template parameter. The overhead is zero: empty structs with <code class="language-plaintext highlighter-rouge">operator()</code> are eliminated via Empty Base Optimization. The <code class="language-plaintext highlighter-rouge">noexcept</code> makes explicit what SDL already guarantees through its C ABI, and gives the compiler room for better optimizations.</p>

<p>Direct construction is not quite enough, though. When <code class="language-plaintext highlighter-rouge">SDL_CreateWindow</code> fails, it returns <code class="language-plaintext highlighter-rouge">nullptr</code> and sets an internal error string. The caller has to check afterward, and <code class="language-plaintext highlighter-rouge">SDL_GetError()</code> must be read immediately after the failing call, because later SDL calls overwrite it. A factory function handles this:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[[</span><span class="n">nodiscard</span><span class="p">]]</span> <span class="kr">inline</span> <span class="n">WindowPtr</span> <span class="nf">make_window</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">title</span><span class="p">,</span>
                                           <span class="kt">uint32_t</span> <span class="n">x</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">y</span><span class="p">,</span>
                                           <span class="kt">uint32_t</span> <span class="n">width</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">height</span><span class="p">,</span>
                                           <span class="kt">uint32_t</span> <span class="n">flags</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">auto</span><span class="o">*</span> <span class="n">raw</span> <span class="o">=</span> <span class="n">SDL_CreateWindow</span><span class="p">(</span><span class="n">title</span><span class="p">.</span><span class="n">c_str</span><span class="p">(),</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">,</span> <span class="n">flags</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">raw</span><span class="p">)</span> <span class="k">throw</span> <span class="n">SDLError</span><span class="p">(</span><span class="s">"SDL_CreateWindow"</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">WindowPtr</span><span class="p">(</span><span class="n">raw</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Either <code class="language-plaintext highlighter-rouge">make_window</code> returns a valid object, or it throws. No null check at the call site, no chance of an invalid <code class="language-plaintext highlighter-rouge">unique_ptr</code> making it into the rest of the code.</p>

<p><code class="language-plaintext highlighter-rouge">SDL_Init</code> and <code class="language-plaintext highlighter-rouge">SDL_Quit</code> are a special case: they have process-wide effect and do not fit into <code class="language-plaintext highlighter-rouge">unique_ptr</code>. A dedicated class is more direct:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Subsystem</span> <span class="p">{</span>
  <span class="nl">public:</span>
    <span class="k">explicit</span> <span class="n">Subsystem</span><span class="p">(</span><span class="kt">uint32_t</span> <span class="n">flags</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">SDL_Init</span><span class="p">(</span><span class="n">flags</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="k">throw</span> <span class="n">SDLError</span><span class="p">(</span><span class="s">"SDL_Init"</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="o">~</span><span class="n">Subsystem</span><span class="p">()</span> <span class="p">{</span> <span class="n">SDL_Quit</span><span class="p">();</span> <span class="p">}</span>

    <span class="n">Subsystem</span><span class="p">(</span><span class="k">const</span> <span class="n">Subsystem</span><span class="o">&amp;</span><span class="p">)</span>            <span class="o">=</span> <span class="k">delete</span><span class="p">;</span>
    <span class="n">Subsystem</span><span class="o">&amp;</span> <span class="k">operator</span><span class="o">=</span><span class="p">(</span><span class="k">const</span> <span class="n">Subsystem</span><span class="o">&amp;</span><span class="p">)</span> <span class="o">=</span> <span class="k">delete</span><span class="p">;</span>
    <span class="n">Subsystem</span><span class="p">(</span><span class="n">Subsystem</span><span class="o">&amp;&amp;</span><span class="p">)</span>                 <span class="o">=</span> <span class="k">delete</span><span class="p">;</span>
    <span class="n">Subsystem</span><span class="o">&amp;</span> <span class="k">operator</span><span class="o">=</span><span class="p">(</span><span class="n">Subsystem</span><span class="o">&amp;&amp;</span><span class="p">)</span>      <span class="o">=</span> <span class="k">delete</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Subsystem</code> is not moveable because <code class="language-plaintext highlighter-rouge">SDL_Quit</code> affects the whole process. A moved-from object could call <code class="language-plaintext highlighter-rouge">SDL_Quit</code> again on destruction while other parts of the program are still using SDL. Deleting the move operations means there is exactly one instance in exactly one place.</p>

<p>The result looks like this:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="n">sdl2</span><span class="o">::</span><span class="n">Subsystem</span> <span class="n">sdl</span><span class="p">{</span><span class="n">SDL_INIT_VIDEO</span><span class="p">};</span>
    <span class="k">auto</span> <span class="n">window</span>   <span class="o">=</span> <span class="n">sdl2</span><span class="o">::</span><span class="n">make_window</span><span class="p">(</span><span class="s">"Demo"</span><span class="p">,</span> <span class="mi">100</span><span class="p">,</span> <span class="mi">100</span><span class="p">,</span> <span class="mi">640</span><span class="p">,</span> <span class="mi">480</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
    <span class="k">auto</span> <span class="n">renderer</span> <span class="o">=</span> <span class="n">sdl2</span><span class="o">::</span><span class="n">make_renderer</span><span class="p">(</span><span class="n">window</span><span class="p">.</span><span class="n">get</span><span class="p">(),</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="n">SDL_RENDERER_ACCELERATED</span><span class="p">);</span>

    <span class="c1">// Cleanup is automatic, in the right order: renderer, then window, then SDL_Quit.</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The order follows from the declaration order: C++ destroys local variables in reverse. That is exactly the order SDL requires, and the language guarantees it rather than relying on discipline.</p>

<p>The pattern is generic. Any C library that manages resources with create and destroy functions benefits from the same approach: a <code class="language-plaintext highlighter-rouge">Deleter</code> struct with one overload per resource type, <code class="language-plaintext highlighter-rouge">using</code> aliases for <code class="language-plaintext highlighter-rouge">unique_ptr</code>, factory functions that catch <code class="language-plaintext highlighter-rouge">nullptr</code> and throw immediately, and for process-wide init and cleanup, a class with deleted copy and move operations. Write it once and you can forget that SDL2 is a C library.</p>]]></content><author><name></name></author><category term="blog" /><category term="c++" /><category term="sdl2" /><category term="raii" /><summary type="html"><![CDATA[I got into the habit of wrapping C libraries before I start working with them. Not out of principle, but because I have seen the same mistakes often enough.]]></summary></entry><entry><title type="html">When ‘Close to the Hardware’ Isn’t Close Enough</title><link href="https://lmilz.dev/blog/2026/04/19/Embedded-Hello-World.html" rel="alternate" type="text/html" title="When ‘Close to the Hardware’ Isn’t Close Enough" /><published>2026-04-19T00:00:00+00:00</published><updated>2026-04-19T00:00:00+00:00</updated><id>https://lmilz.dev/blog/2026/04/19/Embedded-Hello-World</id><content type="html" xml:base="https://lmilz.dev/blog/2026/04/19/Embedded-Hello-World.html"><![CDATA[<p>I recently bought myself an STM32 Nucleo microcontroller board to play around with. What fascinated me was how much more flexible things are at this level, how much more you can do yourself. With an ESP32 that’s not really the case, you’re always tied to ESP-IDF or some other framework.</p>

<p>I started with a first simple example, the kind everyone knows and has done before: the famous <em>Hello World</em>. It’s simple, and that’s exactly why it’s useful. You don’t learn a language’s syntax with it, it’s too small for that. You learn how to actually <em>use</em> the language. What file format, how to compile, how to link, how to run the result.</p>

<p>That’s why I always reach for <em>Hello World</em> first whenever I pick up a new language or a new environment. It forces me to run the whole build system once before I do anything else. Two things I learned this way that weren’t obvious to me at the start. First: you can learn a surprising amount from a simple example if you take it seriously. Second: simple is almost never really simple. Most things that look easy are easy because someone else hid the complexity for you.</p>

<p>The embedded world has a <em>Hello World</em> too: the blinking LED. Most microcontroller boards have an onboard LED that you turn on and off at some frequency. Sounds trivial. And it is, if you use a Hardware Abstraction Layer (HAL) and some ready-made project template.</p>

<p>I’ve been working in automotive software for years, and before that on physics simulations at university. In my head I’d always been “close to the hardware”, I write embedded software after all, not frontend. <a href="https://lmilz.dev/blog/2025/11/08/From-C-to-Rust-Evolving-Programming-Languages-in-Automotive-Development.html">A while back I wrote about the roles of C, C++, and Rust in automotive</a> and quietly took for granted that “embedded equals close to the hardware”. At some point it hit me that this was a delusion. MCAL, AUTOSAR OS, RTE: there are more layers between me and the silicon than between a web app and the kernel. I wanted to actually get down to the bottom for once. No HAL, no framework, no vendor black box. Just the reference manual and the compiler.</p>

<h2 id="a-blinky-in-rust">A Blinky in Rust</h2>

<p>In the Rust ecosystem the example quickly ends up looking like this:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">#![no_std]</span>
<span class="nd">#![no_main]</span>

<span class="k">use</span> <span class="nn">cortex_m_rt</span><span class="p">::</span><span class="n">entry</span><span class="p">;</span>
<span class="k">use</span> <span class="n">panic_halt</span> <span class="k">as</span> <span class="n">_</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">stm32f4xx_hal</span><span class="p">::{</span><span class="n">pac</span><span class="p">,</span> <span class="nn">prelude</span><span class="p">::</span><span class="o">*</span><span class="p">};</span>

<span class="nd">#[entry]</span>
<span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="k">-&gt;</span> <span class="o">!</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">dp</span> <span class="o">=</span> <span class="nn">pac</span><span class="p">::</span><span class="nn">Peripherals</span><span class="p">::</span><span class="nf">take</span><span class="p">()</span><span class="nf">.unwrap</span><span class="p">();</span>
    <span class="k">let</span> <span class="n">rcc</span> <span class="o">=</span> <span class="n">dp</span><span class="py">.RCC</span><span class="nf">.constrain</span><span class="p">();</span>
    <span class="k">let</span> <span class="n">clocks</span> <span class="o">=</span> <span class="n">rcc</span><span class="py">.cfgr</span><span class="nf">.sysclk</span><span class="p">(</span><span class="mi">48</span><span class="nf">.MHz</span><span class="p">())</span><span class="nf">.freeze</span><span class="p">();</span>

    <span class="k">let</span> <span class="n">gpiob</span> <span class="o">=</span> <span class="n">dp</span><span class="py">.GPIOB</span><span class="nf">.split</span><span class="p">();</span>
    <span class="k">let</span> <span class="k">mut</span> <span class="n">led1</span> <span class="o">=</span> <span class="n">gpiob</span><span class="py">.pb0</span><span class="nf">.into_push_pull_output</span><span class="p">();</span>
    <span class="k">let</span> <span class="k">mut</span> <span class="n">led2</span> <span class="o">=</span> <span class="n">gpiob</span><span class="py">.pb7</span><span class="nf">.into_push_pull_output</span><span class="p">();</span>
    <span class="k">let</span> <span class="k">mut</span> <span class="n">delay</span> <span class="o">=</span> <span class="n">dp</span><span class="py">.TIM1</span><span class="nf">.delay_ms</span><span class="p">(</span><span class="o">&amp;</span><span class="n">clocks</span><span class="p">);</span>

    <span class="k">loop</span> <span class="p">{</span>
        <span class="n">led1</span><span class="nf">.toggle</span><span class="p">();</span>
        <span class="n">delay</span><span class="nf">.delay_ms</span><span class="p">(</span><span class="mi">400u32</span><span class="p">);</span>
        <span class="n">led2</span><span class="nf">.toggle</span><span class="p">();</span>
        <span class="n">delay</span><span class="nf">.delay_ms</span><span class="p">(</span><span class="mi">100u32</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>It’s short, type-safe, and it works. The compiler keeps me from toggling an input pin. The clock config goes through a builder pattern. Pin types carry their configuration in the type system, so <code class="language-plaintext highlighter-rouge">toggle()</code> on an input pin is a compile error. The delay is timed off SYSCLK. And everything you <em>don’t</em> see, the vector table, the reset handler, the copy loop for <code class="language-plaintext highlighter-rouge">.data</code>, the zeroing of <code class="language-plaintext highlighter-rouge">.bss</code>, all of that comes from the <code class="language-plaintext highlighter-rouge">cortex-m-rt</code> crate. The linker just gets a small <code class="language-plaintext highlighter-rouge">memory.x</code> that tells it where flash and RAM are.</p>

<p>That’s exactly the problem I wanted to dig into. Not in the “the HAL is bad” sense (I actually came away appreciating it more), but: <strong>I wanted to see what the HAL does for me.</strong> So the same thing again, but in C, no HAL, no CMSIS, just register addresses straight out of the reference manual.</p>

<h2 id="hello-world-embedded">Hello World, embedded</h2>

<p>The board I picked was the Nucleo-F446ZE, and I started reading the docs (Reference Manual RM0390, chapter 6 for RCC and chapter 8 for GPIO).</p>

<p>The blinky itself is quickly explained. Enable the GPIOB clock, configure PB0 as an output, in a loop toggle the output register. In C, with no abstraction, it looks like this. First the registers as macros, then <code class="language-plaintext highlighter-rouge">main()</code>:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">&lt;stdbool.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;stdint.h&gt;</span><span class="cp">
</span>
<span class="cp">#define RCC_BASE   0x40023800UL
#define GPIOB_BASE 0x40020400UL
</span>
<span class="cp">#define RCC_AHB1ENR  (*(volatile uint32_t*)(RCC_BASE  + 0x30UL))
#define GPIOB_MODER  (*(volatile uint32_t*)(GPIOB_BASE + 0x00UL))
#define GPIOB_ODR    (*(volatile uint32_t*)(GPIOB_BASE + 0x14UL))
</span>
<span class="cp">#define RCC_AHB1ENR_GPIOBEN (1UL &lt;&lt; 1)
#define LED_PIN             0U
</span>
<span class="k">static</span> <span class="kt">void</span> <span class="nf">delay</span><span class="p">(</span><span class="k">volatile</span> <span class="kt">uint32_t</span> <span class="n">n</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">while</span> <span class="p">(</span><span class="n">n</span><span class="o">--</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">__asm__</span><span class="p">(</span><span class="s">"nop"</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">RCC_AHB1ENR</span> <span class="o">|=</span> <span class="n">RCC_AHB1ENR_GPIOBEN</span><span class="p">;</span>

    <span class="n">GPIOB_MODER</span> <span class="o">&amp;=</span> <span class="o">~</span><span class="p">(</span><span class="mi">3UL</span> <span class="o">&lt;&lt;</span> <span class="p">(</span><span class="n">LED_PIN</span> <span class="o">*</span> <span class="mi">2</span><span class="p">));</span>
    <span class="n">GPIOB_MODER</span> <span class="o">|=</span>  <span class="p">(</span><span class="mi">1UL</span> <span class="o">&lt;&lt;</span> <span class="p">(</span><span class="n">LED_PIN</span> <span class="o">*</span> <span class="mi">2</span><span class="p">));</span>

    <span class="k">while</span> <span class="p">(</span><span class="nb">true</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">GPIOB_ODR</span> <span class="o">^=</span> <span class="p">(</span><span class="mi">1UL</span> <span class="o">&lt;&lt;</span> <span class="n">LED_PIN</span><span class="p">);</span>
        <span class="n">delay</span><span class="p">(</span><span class="mi">500000</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Three things about this code need explaining.</p>

<p>First: <code class="language-plaintext highlighter-rouge">*(volatile uint32_t*)(...)</code>. That’s memory-mapped I/O in its purest form. The hardware exposes certain addresses that don’t point to ordinary RAM cells, but to registers of the peripherals. Writing to <code class="language-plaintext highlighter-rouge">RCC_AHB1ENR</code> doesn’t mean “write into a memory cell”, it means “tell the RCC block which clocks to enable”. The <code class="language-plaintext highlighter-rouge">volatile</code> cast isn’t a style choice, it’s mandatory. Without <code class="language-plaintext highlighter-rouge">volatile</code>, the compiler wouldn’t care how often you write, it would optimize the accesses away as dead stores, and the blinky would silently do nothing. <code class="language-plaintext highlighter-rouge">volatile</code> is the contract with the compiler: “hands off, every access has a side effect you can’t see.”</p>

<p>Second: initializing <code class="language-plaintext highlighter-rouge">GPIOB_MODER</code>. I clear the two mode bits for PB0 first, then set them to <code class="language-plaintext highlighter-rouge">01</code> (General Purpose Output). Read-modify-write with <code class="language-plaintext highlighter-rouge">&amp;=</code> and <code class="language-plaintext highlighter-rouge">|=</code>, so that other pins in the same register stay untouched. On Cortex-M, by the way, this is <em>not</em> atomic, that’s three instructions (<code class="language-plaintext highlighter-rouge">LDR</code>, <code class="language-plaintext highlighter-rouge">ORR</code>/<code class="language-plaintext highlighter-rouge">BIC</code>, <code class="language-plaintext highlighter-rouge">STR</code>), and an ISR could fire in between. It works here because no interrupts are active during init. If you actually need atomicity, you use the bit-band region (where available, it’s gone on the Cortex-M7) or <code class="language-plaintext highlighter-rouge">LDREX</code>/<code class="language-plaintext highlighter-rouge">STREX</code>. For pure set-or-clear on GPIO output pins there’s also the <code class="language-plaintext highlighter-rouge">BSRR</code> register, which is specifically designed to let you set or reset individual bits atomically in one write, no read-modify-write required.</p>

<p>Third: <code class="language-plaintext highlighter-rouge">delay()</code>. The combination of <code class="language-plaintext highlighter-rouge">volatile</code> on the parameter and the explicit <code class="language-plaintext highlighter-rouge">nop</code> isn’t decoration. Without <code class="language-plaintext highlighter-rouge">volatile</code>, and depending on the optimization level, the compiler may simply skip decrementing the counter, because nobody reads the value. Without the <code class="language-plaintext highlighter-rouge">nop</code>, it’s free to collapse the loop body. Together they force the loop to actually run. The comment “500 ms at 16 MHz” is wishful thinking, since the real duration depends on the optimizer, flash wait states, and the pipeline. For a blinky that’s fine, in production you’d use SysTick.</p>

<p>So much for the functionality. The really interesting question isn’t what’s <em>in</em> <code class="language-plaintext highlighter-rouge">main()</code>, it’s: how does <code class="language-plaintext highlighter-rouge">main()</code> ever get called in the first place? On a PC the operating system does that. On a microcontroller there is no operating system, no loader, no process, nothing that reads in code, allocates memory, or prepares a runtime. Someone has to do all of this by hand. That’s where it got interesting for me.</p>

<h2 id="the-hardware-doesnt-know-about-main">The hardware doesn’t know about <code class="language-plaintext highlighter-rouge">main()</code></h2>

<p>When the ARM Cortex-M4 in the STM32 powers on, it does something very concrete. It reads 4 bytes from address <code class="language-plaintext highlighter-rouge">0x08000000</code> and loads them as the initial stack pointer. Then it reads the next 4 bytes from <code class="language-plaintext highlighter-rouge">0x08000004</code>, interprets them as an address, and jumps there. That’s not a software instruction, that’s circuit logic, set in silicon. Everything that happens after that is software.</p>

<p>One detail that can cost you hours if you don’t know it: bit 0 of the reset vector address has to be set. The Cortex-M4 only knows the Thumb instruction set, and the CPU uses bit 0 of the jump address as a mode bit. If it’s zero, you get a HardFault right after reset. The linker usually takes care of this for you, but anyone who builds the vector table by hand and has to cast a function pointer symbol themselves will learn this one the hard way.</p>

<p>Which gives us a clear requirement: at address <code class="language-plaintext highlighter-rouge">0x08000000</code> exactly the right thing has to be sitting there. This structure is called the vector table, and it’s really just an array of function pointers. First entry is the stack pointer (cast as a function pointer, the hardware doesn’t care about the type, it just reads 4 bytes). Second entry is the address of the reset handler. After that come NMI, HardFault, and the other handlers. On an interrupt, the hardware looks into this table, reads the address, jumps there. It’s a hardware jump table, not a software dispatch.</p>

<p>In code, heavily shortened, it looks like this. The full table also has MemManage, BusFault, UsageFault, SVCall, PendSV, SysTick, and then the roughly 80 STM32-specific IRQs:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">__attribute__</span><span class="p">((</span><span class="n">section</span><span class="p">(</span><span class="s">".isr_vector"</span><span class="p">)))</span>
<span class="kt">void</span> <span class="p">(</span><span class="o">*</span><span class="k">const</span> <span class="n">vector_table</span><span class="p">[])(</span><span class="kt">void</span><span class="p">)</span> <span class="o">=</span> <span class="p">{</span>
    <span class="p">(</span><span class="kt">void</span> <span class="p">(</span><span class="o">*</span><span class="p">)(</span><span class="kt">void</span><span class="p">))(</span><span class="o">&amp;</span><span class="n">_estack</span><span class="p">),</span>
    <span class="n">Reset_Handler</span><span class="p">,</span>
    <span class="n">Default_Handler</span><span class="p">,</span> <span class="cm">/* NMI */</span>
    <span class="n">Default_Handler</span><span class="p">,</span> <span class="cm">/* HardFault */</span>
<span class="p">};</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">section(".isr_vector")</code> attribute matters. It tells the compiler: this data belongs in a specially named section. Where that section ends up in memory, though, isn’t decided here. That was the first moment I realized the compiler and the hardware don’t talk to each other directly. Something’s missing in between.</p>

<p>Since the Cortex-M4 is a licensed ARM core, none of this is STM-specific. It works the same way on boards from NXP, Microchip, or TI. Once you’ve understood it once, you can dive right in on a different board.</p>

<h2 id="sections-floating-in-nothing">Sections floating in nothing</h2>

<p>The STM32 has two memory regions. Flash, non-volatile, starting at <code class="language-plaintext highlighter-rouge">0x08000000</code>. RAM, volatile, starting at <code class="language-plaintext highlighter-rouge">0x20000000</code>. Both on the same 32-bit address bus. From the CPU’s point of view both regions are equally addressable; which addresses point to flash and which to RAM is decided by how the chip is wired.</p>

<p>The C compiler knows none of this. It takes <code class="language-plaintext highlighter-rouge">main.c</code>, produces machine code, puts it into a section called <code class="language-plaintext highlighter-rouge">.text</code>. Constants go into <code class="language-plaintext highlighter-rouge">.rodata</code>, initialized variables into <code class="language-plaintext highlighter-rouge">.data</code>, uninitialized variables into <code class="language-plaintext highlighter-rouge">.bss</code>. These are all just names. The compiler has no idea that <code class="language-plaintext highlighter-rouge">.text</code> is supposed to end up in flash later and <code class="language-plaintext highlighter-rouge">.bss</code> in RAM. It doesn’t even know that flash and RAM exist. The sections have no absolute addresses. They’re just floating in nothing.</p>

<p>So someone has to decide which section ends up at which physical address. That’s the job of the linker script.</p>

<h2 id="the-linker-script-is-the-floor-plan">The linker script is the floor plan</h2>

<p>A linker script is a text file with a <code class="language-plaintext highlighter-rouge">.ld</code> extension. It describes two things: which memory regions exist, and which section goes where.</p>

<p>The line</p>

<pre><code class="language-ld">ENTRY(Reset_Handler)
</code></pre>

<p>tells the linker where the entry point is.</p>

<p>The <code class="language-plaintext highlighter-rouge">MEMORY</code> block lists the physical regions. The numbers come straight from the chip’s datasheet:</p>

<pre><code class="language-ld">MEMORY
{
    FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 512K
    RAM   (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
}
</code></pre>

<p>In the <code class="language-plaintext highlighter-rouge">SECTIONS</code> block every section gets assigned to a region. <code class="language-plaintext highlighter-rouge">.isr_vector</code> goes at the beginning of flash, because that’s where the hardware reads its first 4 bytes. The <code class="language-plaintext highlighter-rouge">KEEP(*(.isr_vector))</code> keeps the linker from throwing the vector table away, since nothing in the C code explicitly references the <code class="language-plaintext highlighter-rouge">vector_table</code> symbol.</p>

<p><code class="language-plaintext highlighter-rouge">.text</code> and <code class="language-plaintext highlighter-rouge">.rodata</code> go into flash, because they’re supposed to be non-volatile. <code class="language-plaintext highlighter-rouge">.bss</code> goes into RAM, because it gets filled with zeros at runtime.</p>

<p>My favorite part is <code class="language-plaintext highlighter-rouge">.data</code>. These are variables with an initial value: <code class="language-plaintext highlighter-rouge">int baud_rate = 9600;</code>. At runtime they need to live in RAM, otherwise they aren’t writable. But the initial value has to be stored somewhere before the board ever gets power. So the initial value has to live in flash and get copied into RAM at startup.</p>

<p>The linker script solves this by giving <code class="language-plaintext highlighter-rouge">.data</code> two addresses. A virtual address (VMA) in RAM, that’s the address the code expects the variable at. And a load address (LMA) in flash, that’s where the initial values physically sit. Who actually does the copying, the linker script doesn’t say. It only emits boundary markers as symbols: <code class="language-plaintext highlighter-rouge">_sidata</code> (start of the initial values in flash), <code class="language-plaintext highlighter-rouge">_sdata</code> and <code class="language-plaintext highlighter-rouge">_edata</code> (start and end in RAM), <code class="language-plaintext highlighter-rouge">_sbss</code> and <code class="language-plaintext highlighter-rouge">_ebss</code> for the <code class="language-plaintext highlighter-rouge">.bss</code> section.</p>

<p>These symbols aren’t variables in the usual sense. They don’t occupy any memory. They’re just numbers that the linker stamps in at the end. In C you access them by declaring them <code class="language-plaintext highlighter-rouge">extern</code> and then taking their address:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">extern</span> <span class="kt">uint32_t</span> <span class="n">_sidata</span><span class="p">;</span>
<span class="k">extern</span> <span class="kt">uint32_t</span> <span class="n">_sdata</span><span class="p">;</span>
<span class="k">extern</span> <span class="kt">uint32_t</span> <span class="n">_edata</span><span class="p">;</span>
</code></pre></div></div>

<p>That confused me for a minute, because they look like variables but aren’t. You never read the <em>value</em> of <code class="language-plaintext highlighter-rouge">_sdata</code>, you always use <code class="language-plaintext highlighter-rouge">&amp;_sdata</code>. The “variable” <em>is</em> its own address.</p>

<h2 id="the-startup-code-is-a-mini-os">The startup code is a mini OS</h2>

<p>The linker script sets the boundaries. Filling everything in is the job of the startup code. Traditionally a file called <code class="language-plaintext highlighter-rouge">startup.s</code> in assembly, nowadays often <code class="language-plaintext highlighter-rouge">startup.c</code> or inline in the same file. I kept mine inline in the blinky, for maximum transparency.</p>

<p>The startup code is the reset handler. It does three things. It copies <code class="language-plaintext highlighter-rouge">.data</code> from flash to RAM so that initialized variables have their initial values. It fills <code class="language-plaintext highlighter-rouge">.bss</code> with zeros so the C standard for uninitialized variables is upheld. Then it calls <code class="language-plaintext highlighter-rouge">main()</code>. With C++ there’s a fourth step: calling global constructors, which runs through a section called <code class="language-plaintext highlighter-rouge">__init_array</code>. In an AUTOSAR project this exact work lives inside the supplier’s startup code and runs before <code class="language-plaintext highlighter-rouge">EcuM_Init</code> ever sees a register.</p>

<p>In the blinky it looks like this:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">Reset_Handler</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">uint32_t</span><span class="o">*</span> <span class="n">src</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">_sidata</span><span class="p">;</span>
    <span class="kt">uint32_t</span><span class="o">*</span> <span class="n">dst</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">_sdata</span><span class="p">;</span>
    <span class="k">while</span> <span class="p">(</span><span class="n">dst</span> <span class="o">&lt;</span> <span class="o">&amp;</span><span class="n">_edata</span><span class="p">)</span> <span class="p">{</span>
        <span class="o">*</span><span class="n">dst</span><span class="o">++</span> <span class="o">=</span> <span class="o">*</span><span class="n">src</span><span class="o">++</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="n">dst</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">_sbss</span><span class="p">;</span>
    <span class="k">while</span> <span class="p">(</span><span class="n">dst</span> <span class="o">&lt;</span> <span class="o">&amp;</span><span class="n">_ebss</span><span class="p">)</span> <span class="p">{</span>
        <span class="o">*</span><span class="n">dst</span><span class="o">++</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="n">main</span><span class="p">();</span>

    <span class="k">while</span> <span class="p">(</span><span class="nb">true</span><span class="p">)</span> <span class="p">{</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Two things stood out to me when I wrote this for the first time.</p>

<p>First: the startup code uses the linker symbols directly as loop bounds. That’s the contract between the two files. The linker script promises that the symbols are there and point to the right addresses. The startup code just trusts it blindly. If you rename the symbols in the linker script, the startup code breaks silently. Nothing warns you. As an aside: the copy loop runs over <code class="language-plaintext highlighter-rouge">uint32_t*</code>, not <code class="language-plaintext highlighter-rouge">uint8_t*</code>. That’s faster, one word per bus transaction instead of four bytes, and it works because the linker aligns the sections to 4 bytes. Unaligned 32-bit accesses can trigger a fault on Cortex-M depending on the configuration.</p>

<p>Second: the <code class="language-plaintext highlighter-rouge">while (true) {}</code> after <code class="language-plaintext highlighter-rouge">main()</code>. On a PC, <code class="language-plaintext highlighter-rouge">main()</code> returns to the operating system. Here there is no operating system. If <code class="language-plaintext highlighter-rouge">main()</code> accidentally returns, the processor has to go somewhere. The infinite loop is insurance against it running wild through memory.</p>

<p>The only reason the startup code can run at all, before the C environment is set up, is that it uses only stack-local variables. And the stack already works because the hardware loaded the stack pointer from the vector table at reset. The whole sequence is a domino chain. Each step has exactly the one precondition the previous step just created.</p>

<h2 id="from-c-to-bytes-in-flash">From <code class="language-plaintext highlighter-rouge">.c</code> to bytes in flash</h2>

<p>What happens between source and a blinking LED is also not one step, but several. The preprocessor resolves includes and macros. The compiler translates each <code class="language-plaintext highlighter-rouge">.c</code> file individually into assembly, architecture-specific via <code class="language-plaintext highlighter-rouge">-mcpu=cortex-m4 -mthumb</code>. The assembler produces object files in ELF format, with relative addresses and unresolved symbols. The linker collects all sections of the same name from all object files, assigns them absolute addresses via the linker script, and resolves the symbols. What used to say “jump to <code class="language-plaintext highlighter-rouge">delay</code>” now carries the concrete flash address of <code class="language-plaintext highlighter-rouge">delay</code>.</p>

<p>The final product is an ELF file. It contains the machine code, but also program headers (which bytes go where in memory), the symbol table (function and variable names with their addresses, for the debugger), and optionally debug information (the mapping of machine code to source lines). A <code class="language-plaintext highlighter-rouge">.bin</code> file, which you produce via <code class="language-plaintext highlighter-rouge">objcopy -O binary</code>, is just the raw bytes without any metadata.</p>

<p>To flash it, you use a tool like probe-rs or OpenOCD. The tool talks over a debug adapter (ST-Link, J-Link, or CMSIS-DAP) to the Cortex-M4’s SWD port (Serial Wire Debug). The debug port has direct access to the entire address space of the chip, regardless of whether the CPU is running. The tool reads the ELF file, extracts the program headers, writes the bytes into flash, and triggers a reset. After which the whole cycle starts over. Hardware reads the vector table, jumps to the reset handler, startup initializes, <code class="language-plaintext highlighter-rouge">main()</code> runs, LED blinks.</p>

<p>I want to stress again that this is a little learning project of mine, not production code. In production you’d put an abstraction on top, either a vendor HAL (for example ST’s) or the Cortex Microcontroller Software Interface Standard (CMSIS), which is vendor-agnostic.</p>

<h2 id="what-i-took-away">What I took away</h2>

<p>Two things stuck with me.</p>

<p>The first: a <em>Hello World</em> is very much not a waste of time if you take it seriously. I could have written the blinky with a HAL in five lines, done. Instead I wrote the vector table myself, the reset handler, read the linker script, understood the boundary markers. And I learned more about the system than I would have in weeks of framework tutorials.</p>

<p>The second: simple is almost never actually simple. The blinky with a HAL isn’t any easier than the blinky with registers, it’s just further away from what’s actually happening. Between <code class="language-plaintext highlighter-rouge">make flash</code> and a blinking LED sit the linker script, the startup code, the ELF, the flashing, the hardware reset. All of this is there, even when you don’t see it.</p>

<p>For those of us in automotive this is particularly interesting. In a classical AUTOSAR project we never see the vector table, never see the reset handler, never see the linker script. The MCAL, the supplier’s startup code, the OS init, BswM scheduling: all of that arrives as a black box, and we write runnables that get called from the RTE. Between power-on and <code class="language-plaintext highlighter-rouge">Rte_MainFunction_*</code> there’s the exact same chain as here. Hardware reads 4 bytes, jumps to the reset handler, someone initializes <code class="language-plaintext highlighter-rouge">.data</code> and <code class="language-plaintext highlighter-rouge">.bss</code>, someone calls the OS init, someone starts the tasks. It’s just that each of those steps is buried inside an AUTOSAR configuration we normally don’t open. Once you’ve built this foundation yourself, you read an MCAL doc differently. It also sharpens the language question. <a href="https://lmilz.dev/blog/2025/11/08/From-C-to-Rust-Evolving-Programming-Languages-in-Automotive-Development.html">In the earlier post I argued that C, C++, and Rust each have their layer</a>, but the layer we usually work on in AUTOSAR is several steps above the one where that choice really matters. The reset handler, the <code class="language-plaintext highlighter-rouge">.data</code> copy, the linker script, those are all C, regardless of what we write on top. The supplier picks the language down there, we don’t.</p>

<p>Not seeing it is comfortable as long as everything works. The moment it doesn’t, you have to go a level deeper. And then it’s good to have been there before.</p>]]></content><author><name></name></author><category term="blog" /><category term="embedded" /><category term="c" /><category term="stm32" /><summary type="html"><![CDATA[I recently bought myself an STM32 Nucleo microcontroller board to play around with. What fascinated me was how much more flexible things are at this level, how much more you can do yourself. With an ESP32 that’s not really the case, you’re always tied to ESP-IDF or some other framework.]]></summary></entry><entry><title type="html">From Read-It-Later to Zettelkasten: Automating My Reading Workflow with n8n</title><link href="https://lmilz.dev/blog/2026/03/16/From-Read-It-Later-to-Zettelkasten-Automating-My-Reading-Workflow-with-n8n.html" rel="alternate" type="text/html" title="From Read-It-Later to Zettelkasten: Automating My Reading Workflow with n8n" /><published>2026-03-16T00:00:00+00:00</published><updated>2026-03-16T00:00:00+00:00</updated><id>https://lmilz.dev/blog/2026/03/16/From-Read-It-Later-to-Zettelkasten-Automating-My-Reading-Workflow-with-n8n</id><content type="html" xml:base="https://lmilz.dev/blog/2026/03/16/From-Read-It-Later-to-Zettelkasten-Automating-My-Reading-Workflow-with-n8n.html"><![CDATA[<h2 id="background">Background</h2>

<p>Over the past few weeks I ran into a familiar problem: my personal projects started to feel like work.</p>

<p>Family, work, and then spending evenings on side projects slowly drained my motivation.</p>

<p>So I asked myself: what is actually bothering me, and where does it make sense to invest personal energy? The answer came quickly: my homelab. Not because I didn’t have one, but because I had been neglecting it. A few Docker containers were running on the Raspberry Pi that I had tried out and then forgotten about. The NAS, which was supposed to handle backups, never really worked as planned. And my self-hosted Git instance was only reachable within my home network, which made it almost useless for day-to-day work.</p>

<p>The idea was clear: build a simple setup that automates a lot, is as open source as possible, and takes privacy seriously.</p>

<h2 id="the-stack">The Stack</h2>

<p>The goal was a small, practical stack that solves real problems rather than hosting everything under the sun.</p>

<p>As a foundation I rented a VPS from a German provider for a few euros a month. Everything runs behind Caddy as a reverse proxy, which also handles HTTPS automatically without any configuration on my part.</p>

<p>The first thing I set up was Forgejo as a Git server, reachable via its own subdomain. That sounds small, but for me it made a real difference: I can now work on private projects from anywhere, whether on a train using a hotspot or somewhere else. At the same time I moved my website from GitHub Pages to the new repo and set up a deploy workflow so that the site gets automatically built and deployed on every push. That actually turned out to be smoother than using GitHub Pages.</p>

<p>After that came CommaFeed as an RSS reader and Readeck as a read-it-later app. Both were set up quickly, and the value was immediately noticeable: I can now read anywhere, save articles, and add notes directly.</p>

<p>Up to this point: solid, but unremarkable self-hosting. The interesting part came after.</p>

<h2 id="the-problem-with-notes">The Problem with Notes</h2>

<p>I read a lot. And like most developers, I have a familiar problem: I save articles, read them, take notes, and then they disappear into the void. The highlights collect dust, the insights fade. I do use the Zettelkasten method for this. But sometimes I simply lacked the focus and time to turn my notes into actual Zettelkasten entries.</p>

<p>While thinking about which other containers I wanted to host, I remembered n8n. I had watched several YouTube videos about what n8n can do.</p>

<p>So I hosted that too and started experimenting. My goal was clear: everything I read and find valuable should flow automatically into my Zettelkasten. Not as a bland summary, but as a thoughtful note that reflects my own thinking and highlights.</p>

<h2 id="the-idea">The Idea</h2>

<p>My setup consists of three components connected through n8n:</p>
<ul>
  <li>Readeck: where I save, read, and highlight articles</li>
  <li>Claude (via the Anthropic API): which generates a Zettelkasten note from my highlights</li>
  <li>Forgejo: where my Zettelkasten lives as Markdown files</li>
</ul>

<p>The key idea is that the workflow never sends the entire article to the language model.
Instead it only sends my highlights, notes, and a short summary. This keeps it privacy-friendly, saves tokens, and most importantly the generated note actually reflects my understanding rather than a generic summary.</p>

<h2 id="the-reading-process">The Reading Process</h2>

<p>Before the workflow can do its thing, I need to do my part. My reading process in Readeck looks like this: I read an article and mark the most important passages as highlights. For each highlight I write a short note with my thoughts. At the end I write an overall summary on one of the highlights, prefixed with “Zusammenfassung:”. Finally I assign the label <code class="language-plaintext highlighter-rouge">Done</code>, which signals to the workflow: this article is ready.</p>

<p>This convention is essential:</p>

<ul>
  <li>the <code class="language-plaintext highlighter-rouge">Done</code> label triggers the workflow</li>
  <li>the <code class="language-plaintext highlighter-rouge">Zusammenfassung:</code> prefix tells the LLM what the central idea is</li>
</ul>

<h2 id="the-readeck-api">The Readeck API</h2>

<p>Readeck offers a REST API and since version 0.22 there are also annotations (highlights with notes) and a Markdown export endpoint that is worth its weight in gold for this workflow.</p>

<p>The endpoint <code class="language-plaintext highlighter-rouge">GET /api/bookmarks?labels=Done</code> returns all articles I have marked for processing. For each bookmark I then fetch the content as Markdown via <code class="language-plaintext highlighter-rouge">GET /api/bookmarks/{id}/article.md</code>. The nice thing about this: the endpoint delivers not just the article text, but also the highlights as <code class="language-plaintext highlighter-rouge">==highlighted text==</code> and my notes as footnotes <code class="language-plaintext highlighter-rouge">[^1]</code>, all in a clean Markdown document with YAML frontmatter.</p>

<h2 id="the-n8n-workflow">The n8n Workflow</h2>

<p>The workflow consists of a loop that processes each article individually. The full chain looks like this:</p>

<p>A <strong>Schedule Trigger</strong> starts the workflow every night at 2am. An <strong>HTTP Request</strong> then fetches all bookmarks with the label <code class="language-plaintext highlighter-rouge">Done</code> from Readeck. A <strong>Loop Over Items</strong> node iterates over each bookmark. Inside the loop another <strong>HTTP Request</strong> fetches the article as Markdown. A <strong>Code Node</strong> parses the Markdown: it extracts the frontmatter, separates the highlights from the regular text, looks for my <code class="language-plaintext highlighter-rouge">Zusammenfassung:</code>, and assembles the input for the language model. A <strong>Basic LLM Chain</strong> sends everything to the Anthropic API and gets a Zettelkasten note back. A second <strong>Code Node</strong> encodes the LLM response as Base64 for the Forgejo API and generates a slugified filename. Three <strong>HTTP Requests</strong> against the Forgejo API create a feature branch, commit the file, and open a pull request. A final <strong>HTTP Request</strong> updates the label in Readeck from <code class="language-plaintext highlighter-rouge">Done</code> to <code class="language-plaintext highlighter-rouge">processed</code>.</p>

<p><img src="/assets/images/n8n_workflow.png" alt="n8n_workflow" /></p>

<h2 id="parsing-extracting-highlights">Parsing: Extracting Highlights</h2>

<p>The core is the Code Node that takes the Markdown article apart. It extracts the highlights (everything between <code class="language-plaintext highlighter-rouge">==</code> characters), maps them to the footnote notes, and specifically looks for my summary.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Extract highlights with footnote number</span>
<span class="kd">const</span> <span class="nx">highlightRegex</span> <span class="o">=</span> <span class="sr">/==</span><span class="se">([^</span><span class="sr">=</span><span class="se">]</span><span class="sr">+</span><span class="se">)</span><span class="sr">==</span><span class="se">(?:\[?\^(\d</span><span class="sr">+</span><span class="se">)\]?)?</span><span class="sr">/g</span><span class="p">;</span>

<span class="c1">// Extract footnotes</span>
<span class="kd">const</span> <span class="nx">footnoteRegex</span> <span class="o">=</span> <span class="sr">/</span><span class="se">\[\^(\d</span><span class="sr">+</span><span class="se">)\]</span><span class="sr">:</span><span class="se">\s</span><span class="sr">*</span><span class="se">(</span><span class="sr">.*</span><span class="se">?)</span><span class="sr">$/gm</span><span class="p">;</span>

<span class="c1">// Find summary</span>
<span class="k">for </span><span class="p">(</span><span class="kd">const</span> <span class="nx">h</span> <span class="k">of</span> <span class="nx">highlightPairs</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">note</span> <span class="o">=</span> <span class="nx">h</span><span class="p">.</span><span class="nx">footnoteNum</span> <span class="p">?</span> <span class="p">(</span><span class="nx">footnoteMap</span><span class="p">[</span><span class="nx">h</span><span class="p">.</span><span class="nx">footnoteNum</span><span class="p">]</span> <span class="o">||</span> <span class="dl">''</span><span class="p">)</span> <span class="p">:</span> <span class="dl">''</span><span class="p">;</span>
  <span class="k">if </span><span class="p">(</span><span class="nx">note</span><span class="p">.</span><span class="nf">toLowerCase</span><span class="p">().</span><span class="nf">startsWith</span><span class="p">(</span><span class="dl">'</span><span class="s1">zusammenfassung:</span><span class="dl">'</span><span class="p">))</span> <span class="p">{</span>
    <span class="nx">zusammenfassung</span> <span class="o">=</span> <span class="nx">note</span><span class="p">.</span><span class="nf">replace</span><span class="p">(</span><span class="sr">/^</span><span class="se">[</span><span class="sr">Zz</span><span class="se">]</span><span class="sr">usammenfassung:</span><span class="se">\s</span><span class="sr">*/i</span><span class="p">,</span> <span class="dl">''</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The output then contains only the frontmatter, my summary, and the individual highlights with notes, no full article text.</p>

<h2 id="the-prompt">The Prompt</h2>

<p>The prompt is designed to generate an atomic Zettelkasten note that reflects my understanding. It took a few iterations before the results felt right.</p>

<p>Key aspects: the LLM should use my summary as a guide and weight it more heavily than individual highlights. The note should distill one central idea, not retell the article. Tags should be thematic and connections to other areas of knowledge should be written as running text. The output format is a fixed Markdown template with YAML frontmatter that fits directly into my Zettelkasten.</p>

<p>A typical note generated by the workflow looks like this:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Deliberate</span><span class="nv"> </span><span class="s">Practice</span><span class="nv"> </span><span class="s">Is</span><span class="nv"> </span><span class="s">the</span><span class="nv"> </span><span class="s">Bottleneck</span><span class="nv"> </span><span class="s">of</span><span class="nv"> </span><span class="s">Learning</span><span class="nv"> </span><span class="s">Software</span><span class="nv"> </span><span class="s">Development"</span>
<span class="na">date</span><span class="pi">:</span> <span class="s2">"</span><span class="s">2025-11-12"</span>
<span class="na">tags</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">learning</span><span class="pi">,</span> <span class="nv">deliberate-practice</span><span class="pi">,</span> <span class="nv">software-craftsmanship</span><span class="pi">]</span>
<span class="na">source</span><span class="pi">:</span> <span class="s2">"</span><span class="s">https://example.com/deliberate-practice-programming"</span>
<span class="na">author</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Jane</span><span class="nv"> </span><span class="s">Doe"</span>
<span class="na">type</span><span class="pi">:</span> <span class="s">literature</span>
<span class="nn">---</span>

<span class="gu">## Core Idea</span>
Improvement in software engineering does not primarily come from accumulated experience but from deliberate practice with feedback. Writing code every day is not the same as intentionally improving one’s skills.

<span class="gu">## Note</span>
The central idea of the article is that many developers spend most of their time performing rather than practicing. Day-to-day work usually focuses on delivering features or fixing bugs, not on deliberately improving specific programming skills.

Deliberate practice requires isolating a skill and working on it intentionally. In software engineering this could mean refactoring a small piece of code multiple times, implementing a known algorithm from memory, or experimenting with different design approaches for the same problem.

Another important aspect is feedback. Without feedback it is difficult to know whether a mental model is actually improving. Code reviews, pair programming, and discussions about architecture decisions can provide exactly this kind of feedback loop.

This aligns with my own experience. Many side projects feel productive but often do not improve specific skills. Short, focused exercises such as refactoring a function or re-implementing a concept from scratch tend to lead to deeper learning.

<span class="gu">## Highlights</span>
<span class="gt">&gt; Expertise is not the result of experience alone, but of structured and deliberate practice.</span>

<span class="ge">*Experience alone does not automatically improve programming skills. Improvement requires consciously focusing on specific aspects of the craft.*</span>
<span class="gt">
&gt; Most professionals spend their time performing, not practicing.</span>

<span class="ge">*In daily work the focus is on delivering results rather than improving individual skills.*</span>
<span class="gt">
&gt; Feedback is the mechanism that turns repetition into learning.</span>

<span class="ge">*Practices like code reviews or pair programming create the feedback loops that deliberate practice depends on.*</span>

<span class="gu">## Connections</span>
The idea of deliberate practice originates from learning psychology and is widely applied in areas such as music or sports. In software engineering it aligns closely with ideas from the software craftsmanship movement, which emphasizes continuous improvement of programming skills. It also connects to practices such as coding katas and focused refactoring sessions, where the primary goal is learning rather than delivering features.
</code></pre></div></div>

<h2 id="the-forgejo-pr-flow">The Forgejo PR Flow</h2>

<p>Instead of committing directly to the main branch, the workflow creates a feature branch and a pull request. This gives me the chance to review and adjust the generated note before merging, in the morning over coffee, without rushing.</p>

<p>The three Forgejo API calls are straightforward: <code class="language-plaintext highlighter-rouge">POST /api/v1/repos/{owner}/{repo}/branches</code> creates a branch like <code class="language-plaintext highlighter-rouge">zettelkasten/2026-03-15-atomic-habits</code>. Then <code class="language-plaintext highlighter-rouge">POST /api/v1/repos/{owner}/{repo}/contents/{path}</code> commits the Markdown file to that branch. Finally <code class="language-plaintext highlighter-rouge">POST /api/v1/repos/{owner}/{repo}/pulls</code> opens the pull request.</p>

<h2 id="cost-and-performance">Cost and Performance</h2>

<p>The Anthropic API is billed through prepaid credits. Running one article through Claude Sonnet costs a few cents, since I only send the highlights and metadata rather than the full article text. A budget of 5 euros per month is more than enough.</p>

<h2 id="stumbling-block">Stumbling Block</h2>

<p>n8n is marketed as a no-code automation platform. But here comes the catch. It works great with mainstream tools like GitHub, which can be connected quickly and easily.</p>

<p>For tools like Forgejo and Readeck you basically have to model the REST calls by hand, exactly as in my workflow. You can import <code class="language-plaintext highlighter-rouge">curl</code> commands, but still need to adjust a few things. That raised a question for me: would a script not have been a better solution here? Because apart from the Anthropic API call, I essentially modelled the REST calls directly in n8n.</p>

<p>That said, I will keep using n8n because I am still just getting started with it. There might be better approaches I have not discovered yet. And it gives me a lot of flexibility without having to wrestle with bash syntax.</p>

<h2 id="conclusion">Conclusion</h2>

<p>The entire workflow took one afternoon, from the first Readeck API call to the finished pull request in Forgejo. Most of the time actually went into refining the prompt and parsing the highlights correctly.</p>

<p>The result is a system that connects my reading process seamlessly to my Zettelkasten, without manually transferring notes.</p>

<p>What I like most: the workflow forces me to read deliberately. I have to set highlights, write notes, and compose a summary before an article gets processed. The language model amplifies my thinking, it does not replace it.</p>

<p>The complete stack is self-hosted: Readeck, n8n, and Forgejo run on my own infrastructure. Only the LLM call goes to the Anthropic API. In the future I could imagine replacing that with a local model too, but the quality of Claude Sonnet for this task is hard to beat right now.</p>

<p>And the original motivation problem? Gone. When a project actually improves something you deal with every day, it becomes fun again.</p>]]></content><author><name></name></author><category term="blog" /><category term="homelab" /><category term="zettelkasten" /><category term="devops" /><summary type="html"><![CDATA[Background]]></summary></entry><entry><title type="html">Beyond Busyness: Why Output Is Not Value</title><link href="https://lmilz.dev/blog/2026/02/01/Beyond-Busyness-Why-Output-Is-Not-Value.html" rel="alternate" type="text/html" title="Beyond Busyness: Why Output Is Not Value" /><published>2026-02-01T00:00:00+00:00</published><updated>2026-02-01T00:00:00+00:00</updated><id>https://lmilz.dev/blog/2026/02/01/Beyond-Busyness-Why-Output-Is-Not-Value</id><content type="html" xml:base="https://lmilz.dev/blog/2026/02/01/Beyond-Busyness-Why-Output-Is-Not-Value.html"><![CDATA[<p>Cal Newport writes about knowledge work. Gene Kim writes about DevOps transformations. Dave Farley writes about continuous delivery. Robert C. Martin writes about software craftsmanship. They come from different worlds, use different vocabularies, and address different audiences.</p>

<p>And yet they are all saying the same thing: producing more is not the same as creating value.</p>

<h2 id="the-common-thread">The Common Thread</h2>

<p>Newport’s <em>Slow Productivity</em> rests on three principles: do fewer things, work at a natural pace, obsess over quality.</p>

<p>Kim’s <em>The Phoenix Project</em> introduces the Three Ways: flow, feedback, and continuous learning. His follow-up <em>The Unicorn Project</em> describes the Five Ideals: locality and simplicity, focus and flow, improvement of daily work, psychological safety, customer focus.</p>

<p>Farley’s <em>Modern Software Engineering</em> argues that software development is an exercise in learning and discovery. He emphasizes iterative work, fast feedback, and empirical approaches over big upfront design.</p>

<p>Martin’s Clean series makes the case for professionalism and quality as non-negotiable foundations. Code should be readable, systems should be maintainable, and developers should take pride in their craft.</p>

<p>Strip away the terminology and the pattern is clear:</p>

<ul>
  <li><strong>Less is more.</strong> Fewer concurrent projects, smaller batches, shorter feedback loops.</li>
  <li><strong>Flow over busyness.</strong> Uninterrupted focus beats fragmented multitasking.</li>
  <li><strong>Quality is not a phase.</strong> It is built in continuously, not inspected in at the end.</li>
  <li><strong>Trust enables speed.</strong> Autonomy and psychological safety are not luxuries, they are prerequisites for good work.</li>
  <li><strong>Learning is the work.</strong> Software development is discovery, not manufacturing.</li>
</ul>

<p>This is not news to anyone who has been paying attention. What is harder to find are concrete examples of what this looks like in practice, especially in regulated industries where process overhead is often seen as inevitable.</p>

<h2 id="when-everything-clicks-a-greenfield-project">When Everything Clicks: A Greenfield Project</h2>

<p>I once worked on a greenfield project where I rebuilt a component from scratch in C. It was embedded software in a regulated industry, which usually means heavy process overhead, but this project was different.</p>

<p>We had a strong product manager who spoke directly with customers and kept us developers in close, ongoing exchange. That meant we understood what customers actually needed without waiting for a formal technical specification. This was unusual. In most projects I had been on, requirements arrived as finished documents, weeks after the customer conversation had happened.</p>

<p>The workflow stayed tight throughout. I would refine a piece of the design, write the tests, implement the change, and open a pull request. Communication happened asynchronously over chat. Code reviews came back fast. We could deploy directly onto test hardware and get immediate feedback, not just on our own component but on how it behaved in the full system. Small changes, integrated frequently, validated against reality rather than assumptions.</p>

<p>What made it work was the presence of trust. No one told me which design pattern to use or how to structure my modules. I could start building before having every detail figured out, try things, fail fast, learn, and iterate.</p>

<p>The principles from the books were not something I applied consciously. They just described what was already happening when the conditions were right.</p>

<h2 id="when-it-is-hard-legacy-systems">When It Is Hard: Legacy Systems</h2>

<p>But here is the question I kept asking myself: does this only work for greenfield projects? What about the legacy systems, the inherited codebases, the regulated environments where everything feels immovable?</p>

<p>I have worked on several of those too. And the answer is: the same principles apply. It just requires a different approach.</p>

<p>The improvements came through deliberate, incremental steps:</p>

<ul>
  <li><strong>Start with requirements.</strong> Reduce them to the essentials. Focus on <em>what</em> and <em>why</em>, not <em>how</em>. Bloated specifications create bloated software and tests. When you force clarity at the requirements level, you often discover that half the complexity was never necessary.</li>
  <li><strong>Improve the documentation.</strong> This sounds mundane, but it is powerful. The act of documenting what the system actually does, not what it was supposed to do, gives you a first sense of where the architecture has drifted and where refactoring is possible.</li>
  <li><strong>Write tests against the legacy code.</strong> Unit tests, component tests, whatever you can get in. They are your safety net for everything that follows. Without tests, every change is a gamble. With tests, you can move with confidence.</li>
  <li><strong>Modularize through refactoring.</strong> And critically: remove unnecessary code entirely. Dead code is not harmless. It is confusion waiting to happen. Every line you delete is a line no one has to understand, maintain, or debug.</li>
  <li><strong>Reduce tool fragmentation.</strong> Instead of a separate tool for every step, limit yourself to two or three. Every tool switch is a context switch, and context switches kill flow.</li>
  <li><strong>Keep the team small and the communication direct.</strong> Every handoff, from requirements engineer to architect, to developer, to tester, loses information. In my experience, a small group of developers working closely with a strong product manager creates far more value than a large team with many handoff points.</li>
</ul>

<p>This is Kim’s Third Ideal: improvement of daily work. The Scout Rule, popularized by Martin: leave the code cleaner than you found it. It is Farley’s emphasis on managing complexity through continuous, small improvements. It is not a separate initiative. It is how the work gets done.</p>

<h2 id="the-myth-of-regulatory-constraints">The Myth of Regulatory Constraints</h2>

<p>There is a common excuse in regulated industries: “We would love to work this way, but compliance makes it impossible.”</p>

<p>I do not buy it.</p>

<p>Standards like ASPICE describe best practices. You need requirements, architecture, design, and tests. You need traceability and verification. But ASPICE does not prescribe how heavyweight those need to be. You can do all of this lean. The standard asks <em>that</em> you demonstrate rigor, not <em>that</em> you drown in documents.</p>

<p>On one project, we replaced huge specification documents with a structured set of few requirements, each linked directly to its test. The auditor accepted it. The team could actually maintain it.</p>

<p>The real barrier is rarely the regulation. It is the assumption that regulation demands bureaucracy. Once teams realize they can improve incrementally, within the rules, the path opens up.</p>

<p>I have seen this happen. Teams that thought they were stuck discovered they had more freedom than they assumed. The constraint was not the standard, it was their interpretation of it.</p>

<p>Kim describes exactly this in <em>The Phoenix Project</em>: the bottleneck is rarely where you think it is. Often the biggest constraint is not technical or regulatory, it is organizational belief.</p>

<h2 id="the-work-that-does-not-show-up-on-the-board">The Work That Does Not Show Up on the Board</h2>

<p>There is a less obvious effect of this kind of work, one that affects the people writing the code as much as the code itself.</p>

<p>There is a concept in psychology called self-efficacy: the belief that your actions matter and that you have agency over how you do your work. Research consistently shows it is one of the strongest predictors of motivation, performance, and resilience in knowledge work.</p>

<p>This is what depth over busyness enables. Not just better software, but better working conditions. The sense of ownership over technical decisions. The tight feedback loop. The freedom to explore and recover on your own terms.</p>

<p>The best developers I have worked with all share one quality: they see work that others overlook.</p>

<p>They notice that a module has grown too complex, that an abstraction no longer fits, that a dependency could be removed. They write the test that will save three hours of debugging next month. They improve the documentation so the next person does not have to reverse-engineer the code. They delete the dead code that everyone else steps around.</p>

<p>This work rarely shows up on a board. There is no ticket for “make tomorrow’s feature possible.” No story points for removing confusion. But it is often the reason the next feature can be built cleanly instead of hacked on top of existing debt.</p>

<p>I think of it as gardening. A garden is never “done.” It grows, it changes, and it needs continuous care. Good developers treat codebases the same way. They do not wait for permission to pull weeds. They do not need a feature request to improve the soil. They understand that small, consistent effort prevents the kind of decay that eventually requires a rewrite.</p>

<p>The job of a software engineer is not to produce code. It is to reduce complexity and create value. Doing fewer things is not laziness, it is judgment. Working at a natural pace is not slowness, it is sustainability. Obsessing over quality is not perfectionism, it is professional responsibility.</p>

<h2 id="starting-today">Starting Today</h2>

<p>The conditions for this kind of work can be cultivated. Not everywhere, not always, but more often than we assume. It starts with one project, one team willing to experiment with fewer meetings, smaller batches, more trust.</p>

<p>Newport, Kim, Farley, and Martin come from different worlds. But they all arrived at the same place: less, but better. The only question left is whether we are willing to do less, so we can build something that matters.</p>]]></content><author><name></name></author><category term="blog" /><category term="productivity" /><category term="software-engineering" /><category term="devops" /><summary type="html"><![CDATA[Cal Newport writes about knowledge work. Gene Kim writes about DevOps transformations. Dave Farley writes about continuous delivery. Robert C. Martin writes about software craftsmanship. They come from different worlds, use different vocabularies, and address different audiences.]]></summary></entry><entry><title type="html">When ‘It Works’ Is No Longer Enough</title><link href="https://lmilz.dev/blog/2026/01/15/When-It-Works-Is-No-Longer-Enough.html" rel="alternate" type="text/html" title="When ‘It Works’ Is No Longer Enough" /><published>2026-01-15T00:00:00+00:00</published><updated>2026-01-15T00:00:00+00:00</updated><id>https://lmilz.dev/blog/2026/01/15/When-It-Works-Is-No-Longer-Enough</id><content type="html" xml:base="https://lmilz.dev/blog/2026/01/15/When-It-Works-Is-No-Longer-Enough.html"><![CDATA[<h2 id="background-story">Background Story</h2>

<p>It was a relaxed evening on vacation. No todo list, no deadline, no code review. Just me, my laptop, and the idea to code something for fun.</p>

<p>Spoiler: It didn’t stay simple. But that’s exactly why this story is worth telling.</p>

<p>A few days earlier, I had found my old bachelor thesis while cleaning up. The topic back then was a particle simulation. Geometric shapes get randomly distributed in a 2D space. When two particles overlap, they push each other away. Over time, order emerges from chaos. Physicists call this Random Organization.</p>

<p>I really enjoyed that project back then. There were conference talks, even a published paper. I was proud of the physics results.</p>

<p>Less proud of the code.</p>

<p>How did I develop back then? Chaotic. No version control, no tests, no documentation. The result: The code is gone. No backups, nothing.</p>

<p>But the idea was still there. And now I also had the knowledge of how to do it better. So that evening, I started rewriting the simulation. This time properly.</p>

<h2 id="rapid-prototyping">Rapid Prototyping</h2>

<p>Was my old code bad? Yes. But that’s okay. Back then I was a physicist, not a developer. Code was a means to an end.</p>

<p>Today, after several years as a professional developer, I see it differently. Code is communication with the computer, with other developers, with my future self.</p>

<p>So I started fresh. Quick, without thinking too much. Get something running first. The prototype worked. The simulation did what it was supposed to. But I wanted to do better. So I started refactoring.</p>

<h2 id="design-patterns-bringing-structure-to-chaos">Design Patterns: Bringing Structure to Chaos</h2>

<p>The prototype worked. But reading through the code, I realized this can be better. Over the past years, I had learned a lot about design patterns and used many. Patterns aren’t magic. They’re documented solutions to problems other developers had before me. Why reinvent the wheel?</p>

<p>So I looked at the code again. Where does something repeat? Where is the code inflexible? Where are potential bugs lurking?</p>

<h3 id="1-factory-pattern-three-methods-are-two-too-many">1. Factory Pattern: Three Methods Are Two Too Many</h3>

<p>In the prototype, I had three almost identical methods:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="n">Simulation</span><span class="o">::</span><span class="n">initCircleSetup</span><span class="p">(</span><span class="kt">uint16_t</span> <span class="n">num</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">}</span>
<span class="kt">void</span> <span class="n">Simulation</span><span class="o">::</span><span class="n">initRectangleSetup</span><span class="p">(</span><span class="kt">uint16_t</span> <span class="n">num</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">}</span>
<span class="kt">void</span> <span class="n">Simulation</span><span class="o">::</span><span class="n">initSquareSetup</span><span class="p">(</span><span class="kt">uint16_t</span> <span class="n">num</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">}</span>
</code></pre></div></div>

<p>Classic Copy Paste Programming.</p>

<p>Plus there was a string comparison for selecting the type:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="n">cfg</span><span class="p">.</span><span class="n">type</span> <span class="o">==</span> <span class="s">"circle"</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">initCircleSetup</span><span class="p">(</span><span class="n">cfg</span><span class="p">.</span><span class="n">num_particles</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">else</span> <span class="nf">if</span> <span class="p">(</span><span class="n">cfg</span><span class="p">.</span><span class="n">type</span> <span class="o">==</span> <span class="s">"rectangle"</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">initRectangleSetup</span><span class="p">(</span><span class="n">cfg</span><span class="p">.</span><span class="n">num_particles</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">else</span> <span class="nf">if</span> <span class="p">(</span><span class="n">cfg</span><span class="p">.</span><span class="n">type</span> <span class="o">==</span> <span class="s">"square"</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">initSquareSetup</span><span class="p">(</span><span class="n">cfg</span><span class="p">.</span><span class="n">num_particles</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">else</span> <span class="p">{</span>
    <span class="n">initCircleSetup</span><span class="p">(</span><span class="n">cfg</span><span class="p">.</span><span class="n">num_particles</span><span class="p">);</span>  <span class="c1">// Silent fallback</span>
<span class="p">}</span>
</code></pre></div></div>

<p>What happens with a typo? “cicle” instead of “circle”? The program silently falls back to the default. No warning, no exception. The bug only gets discovered hours later during debugging.</p>

<p>After refactoring, the code looks like this:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ShapeType.hpp</span>
<span class="k">enum</span> <span class="k">class</span> <span class="nc">ShapeTypes</span> <span class="p">{</span>
    <span class="n">Circle</span><span class="p">,</span>
    <span class="n">Rectangle</span><span class="p">,</span>
    <span class="n">Square</span>
<span class="p">};</span>

<span class="k">class</span> <span class="nc">ShapeType</span> <span class="p">{</span>
<span class="nl">public:</span>
    <span class="k">static</span> <span class="n">Shape</span> <span class="n">create</span><span class="p">(</span><span class="n">ShapeTypes</span> <span class="n">type</span><span class="p">,</span> <span class="k">const</span> <span class="n">Vec</span><span class="o">&amp;</span> <span class="n">pos</span><span class="p">,</span>
                       <span class="kt">double</span> <span class="n">size1</span> <span class="o">=</span> <span class="mf">1.0</span><span class="p">,</span> <span class="kt">double</span> <span class="n">size2</span> <span class="o">=</span> <span class="mf">1.0</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">switch</span> <span class="p">(</span><span class="n">type</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">case</span> <span class="n">ShapeTypes</span><span class="o">::</span><span class="n">Circle</span><span class="p">:</span>
                <span class="k">return</span> <span class="n">Circle</span><span class="p">(</span><span class="n">pos</span><span class="p">,</span> <span class="n">size1</span><span class="p">);</span>
            <span class="k">case</span> <span class="n">ShapeTypes</span><span class="o">::</span><span class="n">Rectangle</span><span class="p">:</span>
                <span class="k">return</span> <span class="n">Rectangle</span><span class="p">(</span><span class="n">pos</span><span class="p">,</span> <span class="n">size1</span><span class="p">,</span> <span class="n">size2</span><span class="p">);</span>
            <span class="k">case</span> <span class="n">ShapeTypes</span><span class="o">::</span><span class="n">Square</span><span class="p">:</span>
                <span class="k">return</span> <span class="n">Square</span><span class="p">(</span><span class="n">pos</span><span class="p">,</span> <span class="n">size1</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="k">throw</span> <span class="n">std</span><span class="o">::</span><span class="n">invalid_argument</span><span class="p">(</span><span class="s">"Unknown shape type"</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">};</span>

<span class="c1">// Simulation.cpp</span>
<span class="kt">void</span> <span class="n">Simulation</span><span class="o">::</span><span class="n">initParticles</span><span class="p">(</span><span class="kt">uint16_t</span> <span class="n">num</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">std</span><span class="o">::</span><span class="n">uniform_real_distribution</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;</span> <span class="n">xDist</span><span class="p">(</span><span class="mf">0.0</span><span class="p">,</span> <span class="n">config</span><span class="p">.</span><span class="n">area_width</span><span class="p">);</span>
    <span class="n">std</span><span class="o">::</span><span class="n">uniform_real_distribution</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;</span> <span class="n">yDist</span><span class="p">(</span><span class="mf">0.0</span><span class="p">,</span> <span class="n">config</span><span class="p">.</span><span class="n">area_height</span><span class="p">);</span>

    <span class="n">particles</span><span class="p">.</span><span class="n">reserve</span><span class="p">(</span><span class="n">num</span><span class="p">);</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">uint16_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">num</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">Vec</span> <span class="n">pos</span><span class="p">(</span><span class="n">xDist</span><span class="p">(</span><span class="n">rng</span><span class="p">),</span> <span class="n">yDist</span><span class="p">(</span><span class="n">rng</span><span class="p">));</span>
        <span class="n">particles</span><span class="p">.</span><span class="n">push_back</span><span class="p">(</span><span class="n">ShapeType</span><span class="o">::</span><span class="n">create</span><span class="p">(</span><span class="n">config</span><span class="p">.</span><span class="n">shape_type</span><span class="p">,</span> <span class="n">pos</span><span class="p">));</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The result: Three methods become one. The DRY principle (Don’t Repeat Yourself) in action. Plus type safety through the enum class. A typo in <code class="language-plaintext highlighter-rouge">ShapeTypes::Cicle</code> leads to a compiler error, not a silent bug at runtime.</p>

<p>Shape creation is now in a central place. If the creation changes, for example because Circle now needs two parameters instead of one, I change it in one place. Not three. And a new shape? Extend the enum, add a case in the factory, done.</p>

<h3 id="2-strategy-pattern-more-than-just-true-or-false">2. Strategy Pattern: More Than Just True or False</h3>

<p>The next problem was the boundary condition. In the prototype, I only had a boolean:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Simulation</span> <span class="p">{</span>
    <span class="kt">bool</span> <span class="n">periodicBoundary</span><span class="p">;</span>  <span class="c1">// true or false, nothing more</span>
<span class="p">};</span>
</code></pre></div></div>

<p>That works for the simple case. But what if I want reflective boundaries or hard walls? With a boolean, that’s not possible. I’d need an enum and then a switch statement that grows with every new boundary. The Simulation class would keep getting bloated.</p>

<p>The solution: The Strategy Pattern. Instead of having the logic in the Simulation class, it gets extracted into separate classes. The simulation only knows the interface and doesn’t know which concrete implementation is behind it.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Boundary.hpp</span>
<span class="k">enum</span> <span class="k">class</span> <span class="nc">BoundaryTypes</span> <span class="p">{</span>
    <span class="n">Hardwall</span><span class="p">,</span>
    <span class="n">Periodic</span><span class="p">,</span>
    <span class="n">Reflective</span>
<span class="p">};</span>

<span class="k">class</span> <span class="nc">Boundary</span> <span class="p">{</span>
<span class="nl">public:</span>
    <span class="k">virtual</span> <span class="o">~</span><span class="n">Boundary</span><span class="p">()</span> <span class="o">=</span> <span class="k">default</span><span class="p">;</span>
    <span class="k">virtual</span> <span class="kt">void</span> <span class="n">apply</span><span class="p">(</span><span class="n">Shape</span><span class="o">&amp;</span> <span class="n">particle</span><span class="p">,</span> <span class="kt">double</span> <span class="n">width</span><span class="p">,</span> <span class="kt">double</span> <span class="n">height</span><span class="p">)</span> <span class="k">const</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">};</span>

<span class="k">class</span> <span class="nc">PeriodicBoundary</span> <span class="o">:</span> <span class="k">public</span> <span class="n">Boundary</span> <span class="p">{</span>
<span class="nl">public:</span>
    <span class="kt">void</span> <span class="n">apply</span><span class="p">(</span><span class="n">Shape</span><span class="o">&amp;</span> <span class="n">particle</span><span class="p">,</span> <span class="kt">double</span> <span class="n">width</span><span class="p">,</span> <span class="kt">double</span> <span class="n">height</span><span class="p">)</span> <span class="k">const</span> <span class="k">override</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">}</span>
<span class="p">};</span>

<span class="k">class</span> <span class="nc">ReflectiveBoundary</span> <span class="o">:</span> <span class="k">public</span> <span class="n">Boundary</span> <span class="p">{</span>
<span class="nl">public:</span>
    <span class="kt">void</span> <span class="n">apply</span><span class="p">(</span><span class="n">Shape</span><span class="o">&amp;</span> <span class="n">particle</span><span class="p">,</span> <span class="kt">double</span> <span class="n">width</span><span class="p">,</span> <span class="kt">double</span> <span class="n">height</span><span class="p">)</span> <span class="k">const</span> <span class="k">override</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">}</span>
<span class="p">};</span>

<span class="k">class</span> <span class="nc">HardwallBoundary</span> <span class="o">:</span> <span class="k">public</span> <span class="n">Boundary</span> <span class="p">{</span>
<span class="nl">public:</span>
    <span class="kt">void</span> <span class="n">apply</span><span class="p">(</span><span class="n">Shape</span><span class="o">&amp;</span> <span class="n">particle</span><span class="p">,</span> <span class="kt">double</span> <span class="n">width</span><span class="p">,</span> <span class="kt">double</span> <span class="n">height</span><span class="p">)</span> <span class="k">const</span> <span class="k">override</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">}</span>
<span class="p">};</span>


<span class="c1">// Simulation.cpp</span>
<span class="kt">void</span> <span class="n">Simulation</span><span class="o">::</span><span class="n">randomPush</span><span class="p">(</span><span class="n">Shape</span><span class="o">&amp;</span> <span class="n">particle1</span><span class="p">,</span> <span class="n">Shape</span><span class="o">&amp;</span> <span class="n">particle2</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// displacement logic</span>
    <span class="n">move</span><span class="p">(</span><span class="n">particle1</span><span class="p">,</span> <span class="n">displacement</span><span class="p">);</span>
    <span class="n">move</span><span class="p">(</span><span class="n">particle2</span><span class="p">,</span> <span class="n">displacement</span> <span class="o">*</span> <span class="p">(</span><span class="o">-</span><span class="mf">1.0</span><span class="p">));</span>

    <span class="c1">// Apply boundary</span>
    <span class="n">boundary_</span><span class="o">-&gt;</span><span class="n">apply</span><span class="p">(</span><span class="n">particle1</span><span class="p">,</span> <span class="n">config</span><span class="p">.</span><span class="n">area_width</span><span class="p">,</span> <span class="n">config</span><span class="p">.</span><span class="n">area_height</span><span class="p">);</span>
    <span class="n">boundary_</span><span class="o">-&gt;</span><span class="n">apply</span><span class="p">(</span><span class="n">particle2</span><span class="p">,</span> <span class="n">config</span><span class="p">.</span><span class="n">area_width</span><span class="p">,</span> <span class="n">config</span><span class="p">.</span><span class="n">area_height</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The beauty of this: The Simulation class never needs to be touched again when a new boundary is added. That’s the Open/Closed Principle in practice. The code is open for extension but closed for modification.</p>

<p>Plus: Each strategy can be tested in isolation. A unit test for PeriodicBoundary, one for ReflectiveBoundary, without having to spin up the entire simulation.</p>

<h3 id="3-builder-pattern-configuration-without-silent-failures">3. Builder Pattern: Configuration Without Silent Failures</h3>

<p>For the simulation configuration, I had a simple struct:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">SimulationConfig</span> <span class="n">config</span><span class="p">;</span>
<span class="n">config</span><span class="p">.</span><span class="n">num_particles</span> <span class="o">=</span> <span class="mi">50</span><span class="p">;</span>
<span class="n">config</span><span class="p">.</span><span class="n">area_width</span> <span class="o">=</span> <span class="mi">50</span><span class="p">;</span>
<span class="n">config</span><span class="p">.</span><span class="n">area_height</span> <span class="o">=</span> <span class="mi">50</span><span class="p">;</span>
<span class="n">config</span><span class="p">.</span><span class="n">max_displacement</span> <span class="o">=</span> <span class="mf">0.5</span><span class="p">;</span>
<span class="n">config</span><span class="p">.</span><span class="n">max_iterations</span> <span class="o">=</span> <span class="mi">5000</span><span class="p">;</span>
<span class="n">config</span><span class="p">.</span><span class="n">boundary_type</span> <span class="o">=</span> <span class="n">BoundaryTypes</span><span class="o">::</span><span class="n">Periodic</span><span class="p">;</span>

<span class="n">Simulation</span> <span class="nf">sim</span><span class="p">(</span><span class="n">config</span><span class="p">);</span>
</code></pre></div></div>

<p>That works. But what if someone forgets to set num_particles? Division by zero, somewhere deep in the code.</p>

<p>The solution is the Builder Pattern. Instead of manipulating the struct directly, there’s a builder that gets configured step by step and validates the input:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">auto</span> <span class="n">config</span> <span class="o">=</span> <span class="n">SimulationConfiguration</span><span class="p">()</span>
    <span class="p">.</span><span class="n">withShapeType</span><span class="p">(</span><span class="n">ShapeTypes</span><span class="o">::</span><span class="n">Circle</span><span class="p">)</span>
    <span class="p">.</span><span class="n">withParticles</span><span class="p">(</span><span class="mi">50</span><span class="p">)</span>
    <span class="p">.</span><span class="n">withArea</span><span class="p">(</span><span class="mf">50.0</span><span class="p">,</span> <span class="mf">50.0</span><span class="p">)</span>
    <span class="p">.</span><span class="n">withMaxDisplacement</span><span class="p">(</span><span class="mf">0.5</span><span class="p">)</span>
    <span class="p">.</span><span class="n">withMaxIterations</span><span class="p">(</span><span class="mi">5000</span><span class="p">)</span>
    <span class="p">.</span><span class="n">withBoundaryType</span><span class="p">(</span><span class="n">BoundaryTypes</span><span class="o">::</span><span class="n">Hardwall</span><span class="p">)</span>
    <span class="p">.</span><span class="n">build</span><span class="p">();</span>

<span class="n">Simulation</span> <span class="nf">sim</span><span class="p">(</span><span class="n">config</span><span class="p">);</span>
</code></pre></div></div>

<p>The trick: The SimulationConfig struct has sensible default values. In build(), it checks if all values are valid. Is a value invalid? Exception. No silent failure. Plus, the code reads almost like a sentence. withParticles(50) is clearer than config.num_particles = 50. The methods document themselves.</p>

<h2 id="the-moment-everything-collapsed">The Moment Everything Collapsed</h2>

<p>I was proud. Factory, Strategy, Builder. Everything there. The code looked professional. I even wrote tests. Time for a demo.</p>

<p>Circles? Perfect.
Rectangles? Works.
Circles AND Rectangles mixed? Segmentation Fault.</p>

<p>No warning. No exception. Just crash.</p>

<p>I stared at the screen. “This can’t be. I did everything right!”</p>

<p>After a debugging session, I found the culprit. The classes for geometric shapes are built according to classic inheritance hierarchy:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Shape</span> <span class="p">{</span>
<span class="nl">public:</span>
    <span class="k">virtual</span> <span class="o">~</span><span class="n">Shape</span><span class="p">()</span> <span class="o">=</span> <span class="k">default</span><span class="p">;</span>
    <span class="k">virtual</span> <span class="kt">bool</span> <span class="n">overlaps</span><span class="p">(</span><span class="k">const</span> <span class="n">Shape</span><span class="o">&amp;</span> <span class="n">other</span><span class="p">)</span> <span class="k">const</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="nl">protected:</span>
    <span class="n">Vec</span> <span class="n">position</span><span class="p">;</span>
<span class="p">};</span>

<span class="k">class</span> <span class="nc">Circle</span> <span class="o">:</span> <span class="k">public</span> <span class="n">Shape</span> <span class="p">{</span>
<span class="nl">public:</span>
    <span class="kt">bool</span> <span class="n">overlaps</span><span class="p">(</span><span class="k">const</span> <span class="n">Shape</span><span class="o">&amp;</span> <span class="n">other</span><span class="p">)</span> <span class="k">const</span> <span class="k">override</span><span class="p">;</span>
<span class="nl">private:</span>
    <span class="kt">double</span> <span class="n">radius</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>

<p>The problem lurked in the overlaps implementation:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">bool</span> <span class="n">Circle</span><span class="o">::</span><span class="n">overlaps</span><span class="p">(</span><span class="k">const</span> <span class="n">Shape</span><span class="o">&amp;</span> <span class="n">other</span><span class="p">)</span> <span class="k">const</span>
<span class="p">{</span>
    <span class="k">const</span> <span class="n">Circle</span><span class="o">*</span> <span class="n">otherCircle</span> <span class="o">=</span> <span class="k">dynamic_cast</span><span class="o">&lt;</span><span class="k">const</span> <span class="n">Circle</span><span class="o">*&gt;</span><span class="p">(</span><span class="o">&amp;</span><span class="n">other</span><span class="p">);</span>

    <span class="n">Vec</span> <span class="n">diff</span> <span class="o">=</span> <span class="n">position</span> <span class="o">-</span> <span class="n">otherCircle</span><span class="o">-&gt;</span><span class="n">position</span><span class="p">;</span>  <span class="c1">// CRASH!</span>
    <span class="kt">double</span> <span class="n">distance</span> <span class="o">=</span> <span class="n">diff</span><span class="p">.</span><span class="n">magnitude</span><span class="p">();</span>

    <span class="k">return</span> <span class="n">distance</span> <span class="o">&lt;</span> <span class="p">(</span><span class="n">radius</span> <span class="o">+</span> <span class="n">otherCircle</span><span class="o">-&gt;</span><span class="n">radius</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The problem: dynamic_cast returns nullptr if other is not a Circle. And what do I do? I dereference the pointer directly without checking. Classic mistake.</p>

<p>The quick solution would be a nullptr check:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">otherCircle</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="n">std</span><span class="o">::</span><span class="n">invalid_argument</span><span class="p">(</span><span class="s">"Circle::overlaps called with non-Circle"</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>But that felt wrong. I had put a lot of effort into design patterns and now I’m supposed to add nullptr checks everywhere? Throw runtime exceptions? That’s exactly the kind of defensive code I wanted to avoid.</p>

<p>The problem wasn’t the implementation. The problem was the design.</p>

<h2 id="the-search-for-a-better-solution">The Search for a Better Solution</h2>

<p>I researched. How do other developers solve this problem?</p>

<p>Double Dispatch? Possible, but lots of boilerplate.
Visitor Pattern? Works, but even more code.</p>

<p>And then I came across std::variant.</p>

<p>A feature from C++17 that I had heard of but never really understood. It comes from functional programming and solves exactly my problem. Not with runtime checks, but at compile time.</p>

<h3 id="what-is-a-sum-type">What Is a Sum Type?</h3>

<p>std::variant is a sum type: A value can be exactly ONE of several types. Not “maybe one”, not “multiple simultaneously”. Exactly one. Always. No nullptr.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">std</span><span class="o">::</span><span class="n">variant</span><span class="o">&lt;</span><span class="kt">int</span><span class="p">,</span> <span class="kt">double</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&gt;</span> <span class="n">value</span><span class="p">;</span>
<span class="n">value</span> <span class="o">=</span> <span class="mi">42</span><span class="p">;</span>         <span class="c1">// now int</span>
<span class="n">value</span> <span class="o">=</span> <span class="mf">3.14</span><span class="p">;</span>       <span class="c1">// now double</span>
<span class="n">value</span> <span class="o">=</span> <span class="s">"hello"</span><span class="p">;</span>    <span class="c1">// now string</span>
</code></pre></div></div>

<p>The crucial difference to classic inheritance: With inheritance, the type hierarchy is open. Anyone can derive new classes from Shape. That makes it easy to add new types. But adding a new operation? Then every class needs to be touched. Every class needs a new method.</p>

<p>With sum types, it’s the opposite. The type set is closed. All types are known at compile time. That makes it harder to add new types because all handlers need to be adjusted. But a new operation? Just write a new handler. The existing code stays untouched.</p>

<p>For my simulation, that means: My shapes are known. Circle, Rectangle, Square. New shapes? Rare. But new operations? Overlap check, rendering, collision response? Those come all the time.</p>

<p>Sum types fit better here than inheritance.</p>

<p>The killer feature: The compiler enforces completeness. Forget a case, you get a compiler error. Not a crash at runtime. Not a nullptr. An error at compile time.</p>

<h3 id="from-classes-to-data-structs">From Classes to Data Structs</h3>

<p>Before, I had a class hierarchy:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Shape</span> <span class="p">{</span> <span class="k">virtual</span> <span class="kt">bool</span> <span class="n">overlaps</span><span class="p">(...)</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="p">};</span>
<span class="k">class</span> <span class="nc">Circle</span> <span class="o">:</span> <span class="k">public</span> <span class="n">Shape</span> <span class="p">{</span> <span class="p">...</span> <span class="p">};</span>
<span class="k">class</span> <span class="nc">Rectangle</span> <span class="o">:</span> <span class="k">public</span> <span class="n">Shape</span> <span class="p">{</span> <span class="p">...</span> <span class="p">};</span>

<span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">unique_ptr</span><span class="o">&lt;</span><span class="n">Shape</span><span class="o">&gt;&gt;</span> <span class="n">particles</span><span class="p">;</span>
</code></pre></div></div>

<p>After, I have pure data structs:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="nc">CircleData</span> <span class="p">{</span>
    <span class="n">Vec</span> <span class="n">position</span><span class="p">;</span>
    <span class="kt">double</span> <span class="n">radius</span><span class="p">;</span>

    <span class="n">CircleData</span><span class="p">(</span><span class="k">const</span> <span class="n">Vec</span><span class="o">&amp;</span> <span class="n">pos</span><span class="p">,</span> <span class="kt">double</span> <span class="n">r</span><span class="p">)</span> <span class="o">:</span> <span class="n">position</span><span class="p">(</span><span class="n">pos</span><span class="p">),</span> <span class="n">radius</span><span class="p">(</span><span class="n">r</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">};</span>

<span class="k">struct</span> <span class="nc">RectangleData</span> <span class="p">{</span>
    <span class="n">Vec</span> <span class="n">position</span><span class="p">;</span>
    <span class="kt">double</span> <span class="n">width</span><span class="p">;</span>
    <span class="kt">double</span> <span class="n">height</span><span class="p">;</span>

    <span class="n">RectangleData</span><span class="p">(</span><span class="k">const</span> <span class="n">Vec</span><span class="o">&amp;</span> <span class="n">pos</span><span class="p">,</span> <span class="kt">double</span> <span class="n">w</span><span class="p">,</span> <span class="kt">double</span> <span class="n">h</span><span class="p">)</span>
        <span class="o">:</span> <span class="n">position</span><span class="p">(</span><span class="n">pos</span><span class="p">),</span> <span class="n">width</span><span class="p">(</span><span class="n">w</span><span class="p">),</span> <span class="n">height</span><span class="p">(</span><span class="n">h</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">};</span>

<span class="k">struct</span> <span class="nc">SquareData</span> <span class="p">{</span>
    <span class="n">Vec</span> <span class="n">position</span><span class="p">;</span>
    <span class="kt">double</span> <span class="n">size</span><span class="p">;</span>

    <span class="n">SquareData</span><span class="p">(</span><span class="k">const</span> <span class="n">Vec</span><span class="o">&amp;</span> <span class="n">pos</span><span class="p">,</span> <span class="kt">double</span> <span class="n">s</span><span class="p">)</span> <span class="o">:</span> <span class="n">position</span><span class="p">(</span><span class="n">pos</span><span class="p">),</span> <span class="n">size</span><span class="p">(</span><span class="n">s</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">};</span>

<span class="k">using</span> <span class="n">Shape</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">variant</span><span class="o">&lt;</span><span class="n">CircleData</span><span class="p">,</span> <span class="n">RectangleData</span><span class="p">,</span> <span class="n">SquareData</span><span class="o">&gt;</span><span class="p">;</span>
</code></pre></div></div>

<p>Since shapes are now pure data structs, not classes with methods, operations are implemented as free functions:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Vec</span> <span class="nf">getPosition</span><span class="p">(</span><span class="k">const</span> <span class="n">Shape</span><span class="o">&amp;</span> <span class="n">s</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">return</span> <span class="n">std</span><span class="o">::</span><span class="n">visit</span><span class="p">([](</span><span class="k">const</span> <span class="k">auto</span><span class="o">&amp;</span> <span class="n">shape</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">shape</span><span class="p">.</span><span class="n">position</span><span class="p">;</span> <span class="p">},</span> <span class="n">s</span><span class="p">);</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="nf">setPosition</span><span class="p">(</span><span class="n">Shape</span><span class="o">&amp;</span> <span class="n">s</span><span class="p">,</span> <span class="k">const</span> <span class="n">Vec</span><span class="o">&amp;</span> <span class="n">pos</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">std</span><span class="o">::</span><span class="n">visit</span><span class="p">([</span><span class="o">&amp;</span><span class="n">pos</span><span class="p">](</span><span class="k">auto</span><span class="o">&amp;</span> <span class="n">shape</span><span class="p">)</span> <span class="p">{</span> <span class="n">shape</span><span class="p">.</span><span class="n">position</span> <span class="o">=</span> <span class="n">pos</span><span class="p">;</span> <span class="p">},</span> <span class="n">s</span><span class="p">);</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="nf">move</span><span class="p">(</span><span class="n">Shape</span><span class="o">&amp;</span> <span class="n">s</span><span class="p">,</span> <span class="k">const</span> <span class="n">Vec</span><span class="o">&amp;</span> <span class="n">delta</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">std</span><span class="o">::</span><span class="n">visit</span><span class="p">([</span><span class="o">&amp;</span><span class="n">delta</span><span class="p">](</span><span class="k">auto</span><span class="o">&amp;</span> <span class="n">shape</span><span class="p">)</span> <span class="p">{</span> <span class="n">shape</span><span class="p">.</span><span class="n">position</span> <span class="o">=</span> <span class="n">shape</span><span class="p">.</span><span class="n">position</span> <span class="o">+</span> <span class="n">delta</span><span class="p">;</span> <span class="p">},</span> <span class="n">s</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>For collision detection, we create a handler with overloaded operator():</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="nc">OverlapHandler</span> <span class="p">{</span>
    <span class="kt">bool</span> <span class="k">operator</span><span class="p">()(</span><span class="k">const</span> <span class="n">CircleData</span><span class="o">&amp;</span> <span class="n">a</span><span class="p">,</span> <span class="k">const</span> <span class="n">CircleData</span><span class="o">&amp;</span> <span class="n">b</span><span class="p">)</span> <span class="k">const</span>
    <span class="p">{</span>
	<span class="c1">// Implementation of circle-circle collision</span>
    <span class="p">}</span>

    <span class="kt">bool</span> <span class="nf">operator</span><span class="p">()(</span><span class="k">const</span> <span class="n">RectangleData</span><span class="o">&amp;</span> <span class="n">a</span><span class="p">,</span> <span class="k">const</span> <span class="n">RectangleData</span><span class="o">&amp;</span> <span class="n">b</span><span class="p">)</span> <span class="k">const</span>
    <span class="p">{</span>
	<span class="c1">// Implementation of rectangle-rectangle collision</span>
    <span class="p">}</span>

    <span class="kt">bool</span> <span class="k">operator</span><span class="p">()(</span><span class="k">const</span> <span class="n">SquareData</span><span class="o">&amp;</span> <span class="n">a</span><span class="p">,</span> <span class="k">const</span> <span class="n">SquareData</span><span class="o">&amp;</span> <span class="n">b</span><span class="p">)</span> <span class="k">const</span>
    <span class="p">{</span>
	<span class="c1">// Implementation of square-square collision</span>
    <span class="p">}</span>

    <span class="kt">bool</span> <span class="k">operator</span><span class="p">()(</span><span class="k">const</span> <span class="n">CircleData</span><span class="o">&amp;</span> <span class="n">c</span><span class="p">,</span> <span class="k">const</span> <span class="n">RectangleData</span><span class="o">&amp;</span> <span class="n">r</span><span class="p">)</span> <span class="k">const</span>
    <span class="p">{</span>
        <span class="c1">// Implementation of circle-rectangle collision</span>
    <span class="p">}</span>

    <span class="kt">bool</span> <span class="k">operator</span><span class="p">()(</span><span class="k">const</span> <span class="n">RectangleData</span><span class="o">&amp;</span> <span class="n">r</span><span class="p">,</span> <span class="k">const</span> <span class="n">CircleData</span><span class="o">&amp;</span> <span class="n">c</span><span class="p">)</span> <span class="k">const</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="p">(</span><span class="o">*</span><span class="k">this</span><span class="p">)(</span><span class="n">c</span><span class="p">,</span> <span class="n">r</span><span class="p">);</span>  <span class="c1">// Symmetric</span>
    <span class="p">}</span>

    <span class="c1">// more combinations</span>
<span class="p">};</span>

<span class="kt">bool</span> <span class="nf">overlaps</span><span class="p">(</span><span class="k">const</span> <span class="n">Shape</span><span class="o">&amp;</span> <span class="n">a</span><span class="p">,</span> <span class="k">const</span> <span class="n">Shape</span><span class="o">&amp;</span> <span class="n">b</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">return</span> <span class="n">std</span><span class="o">::</span><span class="n">visit</span><span class="p">(</span><span class="n">OverlapHandler</span><span class="p">{},</span> <span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>What does all this bring? The most obvious advantage is type safety. With inheritance, type checking happens at runtime with dynamic_cast. With std::variant, it happens at compile time. A forgotten case leads to a compiler error, not a crash at 3 AM in production.</p>

<p>Plus: There’s no nullptr anymore. The value is always one of the defined types. Never null, never undefined. That alone eliminates an entire category of bugs.</p>

<p>Performance is also a factor. Inheritance needs virtual dispatch. The compiler doesn’t know which method gets called, that’s decided at runtime. With std::variant, the compiler knows exactly which function gets called. Direct call instead of indirection. Plus, the data sits on the stack instead of scattered on the heap. More cache-friendly, faster.</p>

<p>The only downside: Adding new types is more expensive. Every handler needs to be adjusted. But for my simulation with three known shapes, that wasn’t a problem.</p>

<h2 id="what-i-really-learned">What I Really Learned</h2>

<p>This project taught me more than just C++ features. It changed how I think about software design.</p>

<h3 id="1-it-works-is-not-the-same-as-its-good">1. “It Works” Is Not the Same as “It’s Good”</h3>

<p>My prototype worked. But the difference between working code and good code is the time I invest in refactoring. Not because someone demands it, but because I want to do better.</p>

<h3 id="2-design-patterns-are-not-an-academic-exercise">2. Design Patterns Are Not an Academic Exercise</h3>

<p>Patterns are documented solutions to problems others had before me. Why reinvent the wheel?</p>

<h3 id="3-paradigms-are-tools-not-religions">3. Paradigms Are Tools, Not Religions</h3>

<p>OOP or FP? The answer is: Both. My final code uses OOP patterns for structure and FP concepts for type safety. The best solution is often a hybrid.</p>

<h3 id="4-the-compiler-is-my-friend">4. The Compiler Is My Friend</h3>

<p>The more I can check at compile time, the less can go wrong at runtime. Type-safe enums instead of strings. std::variant instead of inheritance plus dynamic_cast. Every compiler error is a bug I don’t have to debug.</p>

<h2 id="conclusion-why-its-worth-it">Conclusion: Why It’s Worth It</h2>

<p>What started as a relaxed vacation evening became several weeks of work. Was that too much for a hobby project? Maybe. But it was about getting better. Not perfect. Better.</p>

<p>I learned how std::variant works. I understood when OOP and when FP fits. I experienced that refactoring isn’t a waste of time.</p>

<p>And most importantly: It was fun again.</p>

<p>No deadline. No code review. No stakeholders. Just me, the code, and the question: “How can I make this better?”</p>

<p>This freedom is often missing in daily work. That makes it all the more important to find it in hobby projects.</p>

<p>The complete code is available on <a href="https://github.com/lmilz/random-organization">GitHub</a> and <a href="https://codeberg.org/lmilz/random-organization">Codeberg</a>.</p>]]></content><author><name></name></author><category term="blog" /><category term="c++" /><category term="software-engineering" /><category term="software-architecture" /><summary type="html"><![CDATA[Background Story]]></summary></entry><entry><title type="html">From Defensive Programming to Type-Driven Design</title><link href="https://lmilz.dev/blog/2025/12/11/From-Defensive-Programming-to-Type-Driven-Design.html" rel="alternate" type="text/html" title="From Defensive Programming to Type-Driven Design" /><published>2025-12-11T00:00:00+00:00</published><updated>2025-12-11T00:00:00+00:00</updated><id>https://lmilz.dev/blog/2025/12/11/From-Defensive-Programming-to-Type-Driven-Design</id><content type="html" xml:base="https://lmilz.dev/blog/2025/12/11/From-Defensive-Programming-to-Type-Driven-Design.html"><![CDATA[<h2 id="the-test-that-never-ran">The Test That Never Ran</h2>

<p>Let me be clear upfront: this isn’t about “Rust is better than C.” It’s about recognizing patterns in how we ensure safety. Some languages require discipline and process. Others provide guarantees through their type system. Understanding the difference helps us make better choices about where to invest our effort.</p>

<p>I was working on a linear algebra library in Rust (yes, the 1000th one) when I wrote a test for matrix addition with mismatched dimensions. <code class="language-plaintext highlighter-rouge">cargo build</code> compiled fine, but <code class="language-plaintext highlighter-rouge">cargo test</code> surprised me: the test never ran. Instead:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>error[E0308]: mismatched types
   <span class="nt">--</span><span class="o">&gt;</span> src/matrix.rs:122:29
    |
122 |         <span class="nb">let </span>result <span class="o">=</span> mat1 + mat2<span class="p">;</span>
    |                             ^^^^ expected <span class="sb">`</span>2<span class="sb">`</span>, found <span class="sb">`</span>3<span class="sb">`</span>
</code></pre></div></div>

<p>My first thought was: “Okay, I need to add a check in the library…” Then I stopped. Rust had already caught the error. At compile time. I didn’t need to check anything.</p>

<p>This is Rust’s famous safety, I thought. In C or C++, I would need error handling to catch this case. But then I realized: is that really true? In C, definitely. But in C++, maybe not? This made me think about how different languages handle errors, and more importantly, when those errors are caught. The answer reveals fundamental design philosophies.</p>

<h2 id="the-three-philosophies-of-error-handling">The Three Philosophies of Error Handling</h2>

<p>Before diving into specific languages, let’s establish what we’re actually talking about. Error handling isn’t just about preventing crashes. It’s about where and how we detect problems.</p>

<p><strong>Compile-Time Detection</strong> means errors are caught by the compiler before the program runs. Type mismatches, invalid operations, memory safety violations: these never make it into production.</p>

<p><strong>Runtime Detection</strong> means errors are detected during execution. Invalid inputs, resource failures, unexpected states: these require explicit handling in code.</p>

<p><strong>No Detection</strong> is the most dangerous category. Undefined behavior, silent corruption, race conditions: errors that slip through and cause unpredictable failures.</p>

<p>Each language chooses a different balance. That choice defines not just how we write code, but how we think about safety, performance, and maintainability. Let me show you what this means in practice.</p>
<h2 id="the-c-reality-check">The C Reality Check</h2>

<p>Professionally, I write a lot of C for automotive ECUs. The approach in C to error handling is straightforward: it gives you nothing. No exceptions, no type system enforcement, no safety nets. Just return codes and discipline. So how would I solve the same matrix problem there?</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="kt">int</span><span class="o">*</span> <span class="n">data</span><span class="p">;</span>
    <span class="kt">size_t</span> <span class="n">rows</span><span class="p">;</span>
    <span class="kt">size_t</span> <span class="n">cols</span><span class="p">;</span>
<span class="p">}</span> <span class="n">Matrix</span><span class="p">;</span>

<span class="kt">int</span> <span class="nf">matrix_add</span><span class="p">(</span><span class="n">Matrix</span><span class="o">*</span> <span class="n">result</span><span class="p">,</span> <span class="k">const</span> <span class="n">Matrix</span><span class="o">*</span> <span class="n">a</span><span class="p">,</span> <span class="k">const</span> <span class="n">Matrix</span><span class="o">*</span> <span class="n">b</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// Check 1: Null pointer?</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">result</span> <span class="o">||</span> <span class="o">!</span><span class="n">a</span> <span class="o">||</span> <span class="o">!</span><span class="n">b</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">ERR_NULL_POINTER</span><span class="p">;</span>
    <span class="p">}</span>
    
    <span class="c1">// Check 2: Null data?</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">a</span><span class="o">-&gt;</span><span class="n">data</span> <span class="o">||</span> <span class="o">!</span><span class="n">b</span><span class="o">-&gt;</span><span class="n">data</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">ERR_INVALID_DATA</span><span class="p">;</span>
    <span class="p">}</span>
    
    <span class="c1">// Check 3: Dimension mismatch?</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">a</span><span class="o">-&gt;</span><span class="n">rows</span> <span class="o">!=</span> <span class="n">b</span><span class="o">-&gt;</span><span class="n">rows</span> <span class="o">||</span> <span class="n">a</span><span class="o">-&gt;</span><span class="n">cols</span> <span class="o">!=</span> <span class="n">b</span><span class="o">-&gt;</span><span class="n">cols</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">ERR_DIMENSION_MISMATCH</span><span class="p">;</span>
    <span class="p">}</span>
    
    <span class="c1">// Check 4: Result buffer allocated?</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">result</span><span class="o">-&gt;</span><span class="n">data</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">ERR_NO_MEMORY</span><span class="p">;</span>
    <span class="p">}</span>
    
    <span class="c1">// Finally: The actual addition</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">a</span><span class="o">-&gt;</span><span class="n">rows</span> <span class="o">*</span> <span class="n">a</span><span class="o">-&gt;</span><span class="n">cols</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">result</span><span class="o">-&gt;</span><span class="n">data</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">a</span><span class="o">-&gt;</span><span class="n">data</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">+</span> <span class="n">b</span><span class="o">-&gt;</span><span class="n">data</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
    <span class="p">}</span>
    
    <span class="k">return</span> <span class="n">SUCCESS</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Four defensive checks for a simple operation. And this is just the function itself. Every caller must also check:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Matrix</span> <span class="n">mat1</span><span class="p">,</span> <span class="n">mat2</span><span class="p">,</span> <span class="n">result</span><span class="p">;</span>
<span class="c1">// ... initialization ...</span>

<span class="kt">int</span> <span class="n">err</span> <span class="o">=</span> <span class="n">matrix_add</span><span class="p">(</span><span class="o">&amp;</span><span class="n">result</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">mat1</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">mat2</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="n">err</span> <span class="o">!=</span> <span class="n">SUCCESS</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// Error handling</span>
    <span class="k">switch</span><span class="p">(</span><span class="n">err</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">case</span> <span class="n">ERR_NULL_POINTER</span><span class="p">:</span> <span class="cm">/* ... */</span>
        <span class="k">case</span> <span class="n">ERR_DIMENSION_MISMATCH</span><span class="p">:</span> <span class="cm">/* ... */</span>
        <span class="c1">// ...</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This is defensive programming in its purest form. I must anticipate every conceivable error and catch it. Every function is half error handling, half logic.</p>

<p>What this means in practice is straightforward but demanding. Every function that can fail returns an error code. Every caller must check that code. Forgetting a check? Undefined behavior. Passing wrong types? Undefined behavior. Race condition? Undefined behavior.</p>

<p>In the field of automotive software, this becomes critical. MISRA C exists precisely because C requires perfect discipline. We compensate with strict coding guidelines, static analysis tools, and extensive code reviews. But these are processes trying to replace what the language doesn’t provide.</p>

<h2 id="the-automotive-reality-autosar-misra-and-iso-26262">The Automotive Reality: AUTOSAR, MISRA, and ISO 26262</h2>

<p>Let me be clear about something: I’m simplifying. The automotive software world is more structured than “just use C everywhere.”</p>

<p>We have AUTOSAR Classic for traditional ECUs, built on C with strict architectural patterns. We have AUTOSAR Adaptive for high-performance computing platforms, which allows modern C++. These aren’t just coding standards. They’re complete software architectures that define how components communicate, how resources are managed, and how safety is achieved.</p>

<p>The defensive checks I described aren’t random. They’re systematically required by these architectures. AUTOSAR’s Runtime Environment expects certain error codes. Safety standards like ISO 26262 require certain failure detection mechanisms. MISRA C and MISRA C++ define exactly which language features we can use.</p>

<p>So when I talk about defensive programming versus type-driven design, I’m not suggesting we abandon these standards tomorrow. I’m asking: within these frameworks, where could better language features reduce the burden?</p>

<p>Could AUTOSAR runnables benefit from safer type systems? Could the communication between software components use types instead of runtime validation? Could we use modern C++ features where AUTOSAR Adaptive allows them? Features like <code class="language-plaintext highlighter-rouge">std::optional</code>, <code class="language-plaintext highlighter-rouge">std::variant</code>, or constexpr validation could reduce defensive checks while staying within the standard, instead of staying in the C-style comfort zone.</p>

<h2 id="the-daily-cost-of-defensive-code">The Daily Cost of Defensive Code</h2>

<p>This pattern appears everywhere in typical C codebases. Whether I look at open source projects, tutorial code, or professional embedded software, the ratio is similar.</p>

<p>Let me show you a more automotive-specific example. Consider CAN message handling, something every ECU developer knows:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="kt">uint32_t</span> <span class="n">id</span><span class="p">;</span>
    <span class="kt">uint8_t</span> <span class="n">data</span><span class="p">[</span><span class="mi">8</span><span class="p">];</span>
    <span class="kt">uint8_t</span> <span class="n">length</span><span class="p">;</span>
<span class="p">}</span> <span class="n">CANMessage</span><span class="p">;</span>

<span class="kt">int</span> <span class="nf">validate_and_send_can_message</span><span class="p">(</span><span class="k">const</span> <span class="n">CANMessage</span><span class="o">*</span> <span class="n">msg</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// Check 1: Message exists?</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">msg</span><span class="p">)</span> <span class="k">return</span> <span class="n">ERR_NULL_POINTER</span><span class="p">;</span>
    
    <span class="c1">// Check 2: Valid CAN ID?</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">msg</span><span class="o">-&gt;</span><span class="n">id</span> <span class="o">&gt;</span> <span class="mh">0x1FFFFFFF</span><span class="p">)</span> <span class="k">return</span> <span class="n">ERR_INVALID_ID</span><span class="p">;</span>
    
    <span class="c1">// Check 3: Valid length?</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">msg</span><span class="o">-&gt;</span><span class="n">length</span> <span class="o">&gt;</span> <span class="mi">8</span><span class="p">)</span> <span class="k">return</span> <span class="n">ERR_INVALID_LENGTH</span><span class="p">;</span>
    
    <span class="c1">// Check 4: Standard vs Extended ID consistency?</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">msg</span><span class="o">-&gt;</span><span class="n">id</span> <span class="o">&gt;</span> <span class="mh">0x7FF</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="p">(</span><span class="n">msg</span><span class="o">-&gt;</span><span class="n">id</span> <span class="o">&amp;</span> <span class="mh">0x80000000</span><span class="p">))</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">ERR_ID_FORMAT_MISMATCH</span><span class="p">;</span>
    <span class="p">}</span>
    
    <span class="c1">// Finally: Send the message</span>
    <span class="k">return</span> <span class="n">can_driver_send</span><span class="p">(</span><span class="n">msg</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Every CAN stack has similar functions. Most of the code is validation, not communication. And every project implements these checks slightly differently, leading to subtle bugs across teams.</p>

<p>In my experience with typical embedded C codebases, defensive checks and cleanup logic often make up more than half of the code. The actual business logic gets buried under layers of safety validation. And here’s the dangerous part: forget a single check or a cleanup path, and you have a bug. Every code review must verify these paths. Every test must cover them. This is the cost of safety in C.</p>

<p>Now imagine if the type system could enforce this:</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="nf">StandardCANId</span><span class="p">(</span><span class="nb">u16</span><span class="p">);</span>  <span class="c1">// 11-bit, 0x000-0x7FF</span>
<span class="k">struct</span> <span class="nf">ExtendedCANId</span><span class="p">(</span><span class="nb">u32</span><span class="p">);</span>  <span class="c1">// 29-bit</span>

<span class="k">enum</span> <span class="n">CANId</span> <span class="p">{</span>
    <span class="nf">Standard</span><span class="p">(</span><span class="n">StandardCANId</span><span class="p">),</span>
    <span class="nf">Extended</span><span class="p">(</span><span class="n">ExtendedCANId</span><span class="p">),</span>
<span class="p">}</span>

<span class="k">struct</span> <span class="n">CANMessage</span><span class="o">&lt;</span><span class="k">const</span> <span class="n">N</span><span class="p">:</span> <span class="nb">usize</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="n">id</span><span class="p">:</span> <span class="n">CANId</span><span class="p">,</span>
    <span class="n">data</span><span class="p">:</span> <span class="p">[</span><span class="nb">u8</span><span class="p">;</span> <span class="n">N</span><span class="p">],</span>  <span class="c1">// Length is part of the type</span>
<span class="p">}</span>

<span class="k">impl</span><span class="o">&lt;</span><span class="k">const</span> <span class="n">N</span><span class="p">:</span> <span class="nb">usize</span><span class="o">&gt;</span> <span class="n">CANMessage</span><span class="o">&lt;</span><span class="n">N</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">fn</span> <span class="nf">send</span><span class="p">(</span><span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">(),</span> <span class="n">CANError</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="c1">// No validation needed - invalid messages can't be constructed</span>
        <span class="nf">can_driver_send</span><span class="p">(</span><span class="k">self</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// This compiles:</span>
<span class="k">let</span> <span class="n">msg</span> <span class="o">=</span> <span class="n">CANMessage</span> <span class="p">{</span> 
    <span class="n">id</span><span class="p">:</span> <span class="nn">CANId</span><span class="p">::</span><span class="nf">Standard</span><span class="p">(</span><span class="nf">StandardCANId</span><span class="p">(</span><span class="mi">0x123</span><span class="p">)),</span>
    <span class="n">data</span><span class="p">:</span> <span class="p">[</span><span class="mi">0u8</span><span class="p">;</span> <span class="mi">8</span><span class="p">]</span> 
<span class="p">};</span>

<span class="c1">// This doesn't - length is wrong:</span>
<span class="k">let</span> <span class="n">msg</span> <span class="o">=</span> <span class="n">CANMessage</span> <span class="p">{</span> 
    <span class="n">id</span><span class="p">:</span> <span class="nn">CANId</span><span class="p">::</span><span class="nf">Standard</span><span class="p">(</span><span class="nf">StandardCANId</span><span class="p">(</span><span class="mi">0x123</span><span class="p">)),</span>
    <span class="n">data</span><span class="p">:</span> <span class="p">[</span><span class="mi">0u8</span><span class="p">;</span> <span class="mi">9</span><span class="p">]</span>  <span class="c1">// Compiler error!</span>
<span class="p">};</span>
</code></pre></div></div>

<p>The validation isn’t gone. It’s moved to the constructor of <code class="language-plaintext highlighter-rouge">StandardCANId</code> and <code class="language-plaintext highlighter-rouge">ExtendedCANId</code>:</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">impl</span> <span class="n">StandardCANId</span> <span class="p">{</span>
    <span class="k">fn</span> <span class="nf">new</span><span class="p">(</span><span class="n">id</span><span class="p">:</span> <span class="nb">u16</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="k">Self</span><span class="p">,</span> <span class="n">CANError</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="k">if</span> <span class="n">id</span> <span class="o">&gt;</span> <span class="mi">0x7FF</span> <span class="p">{</span>
            <span class="k">return</span> <span class="nf">Err</span><span class="p">(</span><span class="nn">CANError</span><span class="p">::</span><span class="n">InvalidStandardId</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="nf">Ok</span><span class="p">(</span><span class="nf">StandardCANId</span><span class="p">(</span><span class="n">id</span><span class="p">))</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Once you have a valid <code class="language-plaintext highlighter-rouge">StandardCANId</code>, the compiler guarantees it stays valid. The type system enforces CAN protocol constraints.</p>

<p>This is conceptual. In practice, you’d still need unsafe code to interface with the CAN hardware driver. But the type safety lives in the application layer, where most bugs actually happen.</p>

<p>Now, I need to be realistic here. Rust in automotive production is still early days. There are no ISO 26262 qualified Rust compilers yet. Tool qualification is expensive and time-consuming. The automotive industry moves slowly, and for good reasons.</p>

<p>Rust also has <code class="language-plaintext highlighter-rouge">unsafe</code> blocks where you explicitly opt out of safety guarantees. You still need discipline there. And Rust doesn’t prevent logic errors or deadlocks. It’s not magic.</p>

<p>But here’s what matters: Rust demonstrates that compile-time safety and zero-cost abstractions are possible. Even if we can’t use Rust in production tomorrow, it shows us what modern C++ could be if we used it more fully within AUTOSAR Adaptive contexts.</p>

<h2 id="the-shift-in-thinking">The Shift in Thinking</h2>

<p>This is the moment that hit me. In C, I think: “What can go wrong? What checks do I need?” In Rust, I think: “What do I want to express? What type describes that?”</p>

<p>This is not just less code. It’s a fundamental difference in mental model. With defensive programming in C, I catch errors at runtime. Every developer must think about every edge case. I write a lot of code for safety, and testing proves correctness.</p>

<p>The difference is profound. In defensive programming, safety is a property I must actively maintain through discipline and process. In type-driven design, safety is a property the language guarantees through its type system. With type-driven design in Rust and modern C++, I make certain classes of errors impossible at compile time. The compiler prevents type mismatches, null pointer dereferences, and memory safety violations. Safety comes through design, and types prove these specific properties. They don’t catch logic errors or algorithmic mistakes, but they eliminate entire categories of bugs that plague C codebases.</p>

<h2 id="the-automotive-perspective">The Automotive Perspective</h2>

<p>In typical automotive ECU code, we have thousands of defensive checks scattered throughout the codebase. Everywhere you look, you see the same patterns. Null pointer checks, bounds checks, range checks, state validation checks. All necessary, all manually written, all manually verified in code reviews.</p>

<p>With MISRA C and MISRA C++, code reviews, and static analysis, we try to ensure that no check is forgotten. This is process as a replacement for language features. We build elaborate systems to catch what the language cannot.</p>

<p>In Rust or modern C++, many defensive checks would take different forms:</p>

<p>Null pointer checks could become <code class="language-plaintext highlighter-rouge">Option&lt;T&gt;</code> (explicitly marking that a value might be absent) or references (guaranteeing a value exists). Bounds checks could become compile-time <code class="language-plaintext highlighter-rouge">const</code> sizes or explicit <code class="language-plaintext highlighter-rouge">.get()</code> calls that return <code class="language-plaintext highlighter-rouge">Option</code>. State machines could use the type-state pattern, where each state is a different type and transitions are enforced by the compiler. Value ranges could use newtype patterns, where a type like <code class="language-plaintext highlighter-rouge">Speed0to100(u8)</code> can only be constructed with valid values.</p>

<p>These aren’t just different syntax. They shift validation from runtime checks scattered throughout the code to compile-time constraints or explicit construction points.</p>

<h2 id="the-practical-takeaway">The Practical Takeaway</h2>

<p>My realization is not “Rust is better than C.” It is this: I am spending time writing defensive code that would be unnecessary in modern languages.</p>

<p>This doesn’t mean I should rewrite all our C code tomorrow. But it changes how I think about new components. For the hardware layer, C remains essential for direct register access. There’s no choice there. For the safety layer, I must invest in defensive checks because the language requires it. But for business logic? There, Rust or modern C++ could replace defensive checks with types.</p>

<p>This distinction matters. It means I can choose where to invest effort. Where I must use C, I accept the cost of defensive programming. Where I have flexibility, I can leverage type systems to reduce that cost.</p>

<h2 id="the-elephant-in-the-room-why-not-switch-tomorrow">The Elephant in the Room: Why Not Switch Tomorrow?</h2>

<p>Let me address what I’m not saying. I’m not suggesting we rewrite our ECU software in Rust tomorrow. That would be naive for several reasons:</p>

<p>First, there’s certification. Our tools are qualified according to ISO 26262. Our processes are qualified according to ASPICE. Introducing a new language means re-qualifying everything. That’s years of work and significant investment.</p>

<p>Second, there’s legacy. We have millions of lines of proven C code. It works. It’s tested. It’s certified. Rewriting it would be expensive and risky.</p>

<p>Third, there’s the team. We have engineers trained in C and AUTOSAR. Learning Rust or modern C++ takes time. Not everyone will embrace it.</p>

<p>Fourth, there’s the ecosystem. Our suppliers deliver AUTOSAR components in C. Our customers expect C interfaces. The entire automotive software supply chain is built around C.</p>

<p>So why am I writing this article? Because I believe we should ask different questions. Not “should we switch to Rust” but “where are we paying unnecessary costs with current tools?”</p>

<p>Are there new components where we could use AUTOSAR Adaptive with modern C++ features? Are there application layers where type safety would prevent entire bug categories? Are there areas where the process overhead of defensive C programming outweighs the migration cost?</p>

<p>These are the questions this compiler error made me ask.</p>

<h2 id="the-question-im-left-with">The Question I’m Left With</h2>

<p>How much of my daily work is actually just manual type checking? How many bugs have I fixed that wouldn’t have compiled in Rust? How much code review time do we spend verifying that all defensive checks are present?</p>

<p>These are the real questions this compiler error raised. Not “which language is better,” but “how much of my work could be automated?”</p>

<p>For new projects, I will think more about this: Can I model the problem so that erroneous states are excluded at compile time? For legacy code: Can I at least move the defensive checks into clearly bounded layers instead of scattering them everywhere?</p>

<p>Looking back, this small moment with a matrix library taught me something fundamental. The goal is not to eliminate error handling. The goal is to move it to where it’s most effective. Sometimes that’s runtime checks with discipline and process. Sometimes that’s compile-time guarantees with types and compilers.</p>

<p>Knowing the difference and choosing deliberately makes all the difference.</p>
<h2 id="what-im-taking-away">What I’m Taking Away</h2>

<p>This compiler error taught me to recognize a pattern in my daily work. When I write defensive checks, I should ask: is this a runtime constraint that could fail (like “file not found”), or is it a type constraint that should never occur (like “wrong matrix dimension”)?</p>

<p>Runtime constraints need runtime checks. That’s fine. But type constraints masquerading as runtime checks? That’s where we’re wasting effort.</p>

<p>In new code, I’m trying to push more constraints into types. Not because it’s fashionable, but because every type constraint I encode is one less thing I need to check, test, review, and debug.</p>

<p>In existing code, I’m thinking more about boundaries. Can I at least isolate defensive checks at system boundaries, rather than scattering them throughout? Can I create a safe inner core where certain classes of errors are impossible by construction?</p>]]></content><author><name></name></author><category term="blog" /><category term="c" /><category term="rust" /><category term="software-engineering" /><category term="embedded" /><summary type="html"><![CDATA[The Test That Never Ran]]></summary></entry></feed>