<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="../assets/xml/rss.xsl" media="all"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>TinyComputers.io (Posts about tooling)</title><link>https://tinycomputers.io/</link><description></description><atom:link href="https://tinycomputers.io/categories/tooling.xml" rel="self" type="application/rss+xml"></atom:link><language>en</language><copyright>Contents © 2026 A.C. Jokela 
&lt;!-- div style="width: 100%" --&gt;
&lt;a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/"&gt;&lt;img alt="" style="border-width:0" src="https://i.creativecommons.org/l/by-sa/4.0/80x15.png" /&gt; Creative Commons Attribution-ShareAlike&lt;/a&gt;&amp;nbsp;|&amp;nbsp;
&lt;!-- /div --&gt;
</copyright><lastBuildDate>Sun, 02 Aug 2026 19:12:52 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>The Config Language That Was Already There: A Ballistics REPL Written in Lattice</title><link>https://tinycomputers.io/posts/the-config-language-that-was-already-there.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/the-config-language-that-was-already-there_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;28 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;h2&gt;The Config Language That Was Already There: A Ballistics REPL Written in Lattice&lt;/h2&gt;
&lt;p&gt;Every calculator that gets used seriously eventually grows a configuration language.&lt;/p&gt;
&lt;p&gt;It starts reasonably. You have a solver with fifteen inputs, and typing fifteen command-line flags every time is miserable, so you accept a config file. YAML, probably. Then someone wants two configs that differ in one field, so you add includes, or merge semantics, or a &lt;code&gt;base:&lt;/code&gt; key. Then someone wants a parameter sweep, so you add a templating layer, or you tell them to write a shell loop that generates YAML. Then the YAML needs comments explaining which units each field is in, because YAML has no units and never will. Eventually the configuration language has variables, and interpolation, and a way to express "the same as above but 200 feet per second faster," and you have accidentally built a bad programming language with no debugger.&lt;/p&gt;
&lt;p&gt;I have written that layer. More than once. This is about the time I refused to.&lt;/p&gt;
&lt;p&gt;The result is a ballistics laboratory — a stateful REPL for exterior ballistics — written entirely in &lt;a href="https://lattice-lang.org/"&gt;Lattice&lt;/a&gt;, the small interpreted language I've been building, driving the &lt;a href="https://tinycomputers.io/posts/open-sourcing-a-high-performance-rust-based-ballistics-engine.html"&gt;Rust ballistics engine&lt;/a&gt; I've also been building. The interesting part isn't that the two projects finally met. It's the specific thing I didn't have to build: there is no configuration format. The config language is Lattice, because the REPL is Lattice, and the experiment you're configuring is an ordinary value in the session.&lt;/p&gt;
&lt;p&gt;You don't need to care about ballistics for the argument to land. Substitute your own domain with a solver at the bottom and a pile of parameters on top.&lt;/p&gt;
&lt;div class="download-widget"&gt;
&lt;div class="download-widget-header"&gt;&lt;span&gt;⬇&lt;/span&gt; Get Lattice&lt;/div&gt;
&lt;p&gt;Lattice is free and open source at &lt;a href="https://lattice-lang.org/"&gt;lattice-lang.org&lt;/a&gt;, and there is almost certainly a prebuilt binary for whatever you are reading this on. The current release ships &lt;strong&gt;22 precompiled binaries across 11 platform targets&lt;/strong&gt; — macOS, Linux, Windows, FreeBSD, OpenBSD and NetBSD, on x86_64 and aarch64, plus Linux on RISC-V — with checksums for all of them.&lt;/p&gt;
&lt;p&gt;Each platform gets two builds: &lt;code&gt;clat&lt;/code&gt;, the full interpreter with the REPL, formatter, debugger and documentation tooling, and &lt;code&gt;clat-run&lt;/code&gt;, a runtime-only binary for executing compiled bytecode (655 KB on arm64 macOS). No runtime dependency to install; the ballistics laboratory in this post lives in the source tree under &lt;code&gt;examples/ballistics_lab/&lt;/code&gt;.&lt;/p&gt;
&lt;/div&gt;

&lt;h3&gt;The Shape of the Problem&lt;/h3&gt;
&lt;p&gt;Exterior ballistics is a good stress test for this idea, because the input surface is genuinely large and genuinely fussy.&lt;/p&gt;
&lt;p&gt;A trajectory depends on the projectile (mass, diameter, length, drag model, ballistic coefficient), the rifle (sight height, twist rate and direction, muzzle height), the atmosphere (altitude, temperature, pressure, humidity, latitude), the wind (speed, direction, and whether you're modeling a vertical component), the shot itself (muzzle velocity, zero distance, maximum range, shooting angle, cant, azimuth), and the solver (method, time step, tolerances). Miss a unit conversion and you get an answer that is confidently, silently wrong.&lt;/p&gt;
&lt;p&gt;Here is the state of a fresh session, printed by the laboratory's &lt;code&gt;:show&lt;/code&gt; command:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;Experiment&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;175&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;yards&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;projectile&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;175&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;mass&lt;/span&gt;&lt;span class="w"&gt;                 &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;175.00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gr&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;diameter&lt;/span&gt;&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.31&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.24&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;drag&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;G7&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;ballistic&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;coefficient&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.24&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;rifle&lt;/span&gt;&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;inch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rifle&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;sight&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.50&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;twist&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;8.00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;twist&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;right&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;altitude&lt;/span&gt;&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;273.40&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;yd&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;59.00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;pressure&lt;/span&gt;&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;29.92&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inHg&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;humidity&lt;/span&gt;&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;50.00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mf"&gt;45.00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;deg&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;muzzle&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;velocity&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2700.00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ft&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;maximum&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;range&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1000.00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;yd&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;zero&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;100.00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;yd&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;solver&lt;/span&gt;&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rk45&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That is a lot of fields, and every one of them is a place where a config format would want a schema, a units convention, and a validation pass. This is exactly the shape of problem that produces a bespoke DSL.&lt;/p&gt;
&lt;h3&gt;What Running It Looks Like&lt;/h3&gt;
&lt;p&gt;The laboratory starts from a source checkout with a launcher that finds the interpreter and the engine:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;./scripts/ballistics-lab.sh
&lt;/pre&gt;&lt;/div&gt;

&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nv"&gt;ballistics&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;lab&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;backend&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;stack&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;vm&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;engine&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="nv"&gt;Users&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;alex&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;projects&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;ballistics&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;engine&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;target&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;release&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;ballistics&lt;/span&gt;
&lt;span class="nv"&gt;Ballistics&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;laboratory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;—&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Lattice&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;v0&lt;/span&gt;.&lt;span class="mi"&gt;6&lt;/span&gt;.&lt;span class="mi"&gt;4&lt;/span&gt;
&lt;span class="nv"&gt;Engine&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;...&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;ballistics&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;engine&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;target&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;release&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;ballistics&lt;/span&gt;
&lt;span class="nv"&gt;Type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;:&lt;span class="nv"&gt;help&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;commands&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;enter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Lattice&lt;/span&gt;.&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Ctrl&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;D&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;exit&lt;/span&gt;.
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The prompt accepts two kinds of input. Colon commands drive the session, and everything else is Lattice.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;:solve&lt;/code&gt; runs the current experiment through the engine:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;ballistics&amp;gt; :solve
Run: 175 gr match at 1,000 yards
  backend          : process-json-v1
  engine           : 0.30.1
  schema           : 1
  termination      : max_range
  actual range     : 1000.00 yd
  time of flight   : 1.71 s
  terminal velocity: 1145.95 ft/s
  terminal energy  : 510.19 ft-lb
  stability factor : 3.80
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;:at&lt;/code&gt; interrogates a single point on the solved trajectory:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;ballistics&amp;gt; :at 300 yd
Observation
  distance: 300.00 yd
  time    : 0.37 s
  velocity: 2162.46 ft/s
  energy  : 1816.78 ft-lb
  drop    : +18.91 in
  windage : -6.71 in
  Mach    : 1.93
  flags   : none
Signs: drop/windage + above/right, - below/left
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And &lt;code&gt;:dope&lt;/code&gt; produces the table a shooter actually tapes to a rifle:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;ballistics&amp;gt; :dope 100 600 100 yd
DOPE: 175 gr match at 1,000 yards
Distance (yd) | Drop (in) | Come-up (mil) | Velocity (ft/s) | Energy (ft-lb) | Time (s) | Mach
--------------+-----------+---------------+-----------------+----------------+----------+-----
       100.00 |     +1.50 |         -0.42 |         2513.37 |        2454.23 |     0.12 | 2.25
       200.00 |     +7.01 |         -0.97 |         2334.07 |        2116.56 |     0.24 | 2.09
       300.00 |    +18.91 |         -1.75 |         2162.46 |        1816.78 |     0.37 | 1.93
       400.00 |    +38.26 |         -2.66 |         1998.49 |        1551.71 |     0.52 | 1.79
       500.00 |    +66.33 |         -3.69 |         1841.19 |        1317.05 |     0.67 | 1.65
       600.00 |   +104.69 |         -4.85 |         1689.82 |        1109.39 |     0.84 | 1.51
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;There's a companion table for wind, which is where shooters actually lose their afternoons:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;ballistics&amp;gt; :wind-card 200 600 200 yd
Wind card
Series | Trajectory                  | Wind (mph) | From (deg) | Distance (yd) | Windage (in) | Wind hold (mil)
-------+-----------------------------+------------+------------+---------------+--------------+----------------
  0.00 | 175 gr match at 1,000 yards |      10.00 |      90.00 |        200.00 |        -2.87 |           +0.40
  0.00 | 175 gr match at 1,000 yards |      10.00 |      90.00 |        400.00 |       -12.39 |           +0.86
  0.00 | 175 gr match at 1,000 yards |      10.00 |      90.00 |        600.00 |       -30.23 |           +1.40
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Note the leading &lt;code&gt;Series&lt;/code&gt; column, which is &lt;code&gt;0.00&lt;/code&gt; throughout because there's one trajectory loaded. It's there because these tables are built to hold several trajectories at once and label each row with its source — the tabular output is a &lt;em&gt;view over a collection of runs&lt;/em&gt;, not a formatted dump of the most recent one. That distinction is going to matter in a moment.&lt;/p&gt;
&lt;p&gt;So far this is a competent CLI. Nothing here requires a programming language.&lt;/p&gt;
&lt;h3&gt;The Part That Requires a Programming Language&lt;/h3&gt;
&lt;p&gt;Now change something.&lt;/p&gt;
&lt;p&gt;Not by editing a file and restarting. Not by passing a flag. By typing at the prompt, in the same language the laboratory is written in:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;ballistics&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sessions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;experiment_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lab&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ballistics&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;better&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;projectile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"230 gr high-BC"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;230&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.308&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.368&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"G7"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.55&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;ballistics&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;alt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;experiment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"230 gr high-BC load"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;better&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rifle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;atmosphere&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;winds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;solver&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ballistics&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sessions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace_experiment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lab&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ballistics&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;solve&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;230&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;high&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;BC&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;load&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;flight&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.44&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;terminal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;velocity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1601.26&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ft&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;terminal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;energy&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1309.24&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ft&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;lb&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Same rifle, same atmosphere, same wind, same solver, same 1,000 yards. A heavier bullet with a better drag coefficient arrives &lt;em&gt;sooner&lt;/em&gt; (1.44 s versus 1.71 s), still supersonic (1601 ft/s versus 1146), carrying two and a half times the terminal energy (1309 ft-lb versus 510).&lt;/p&gt;
&lt;p&gt;Look at what that interaction actually required. &lt;code&gt;base.rifle&lt;/code&gt;, &lt;code&gt;base.atmosphere&lt;/code&gt;, &lt;code&gt;base.winds&lt;/code&gt;, &lt;code&gt;base.shot&lt;/code&gt;, &lt;code&gt;base.solver&lt;/code&gt; — five subsystems reused verbatim, by &lt;em&gt;name&lt;/em&gt;, because the experiment is a structured value and not a document. There was no merge semantics to design. No &lt;code&gt;extends:&lt;/code&gt; key. No question about whether a null in the override means "unset this" or "inherit this." I destructured a record and rebuilt it with one field changed, which is a thing programming languages have been good at for fifty years.&lt;/p&gt;
&lt;p&gt;That is the entire argument. The configuration language you were about to write is a worse version of a language you already have.&lt;/p&gt;
&lt;p&gt;Units get the same treatment. &lt;code&gt;gr(230)&lt;/code&gt;, &lt;code&gt;inches(0.308)&lt;/code&gt;, &lt;code&gt;fps(2700)&lt;/code&gt;, &lt;code&gt;yards(1000)&lt;/code&gt; aren't string tags to be parsed and validated — they're constructors producing distinct quantity values with SI internals and a preferred display unit. A YAML field called &lt;code&gt;mass: 230&lt;/code&gt; has to be documented as grains and validated at load time. &lt;code&gt;gr(230)&lt;/code&gt; carries that with it, and the failure mode for passing a length where a mass belongs is an evaluation error, not a plausible-looking wrong trajectory.&lt;/p&gt;
&lt;h3&gt;"You Didn't Avoid Writing a Config Language, You Wrote a Whole Language"&lt;/h3&gt;
&lt;p&gt;This is the obvious objection and it deserves a straight answer rather than a clever one.&lt;/p&gt;
&lt;p&gt;If the choice were &lt;em&gt;write a YAML schema&lt;/em&gt; versus &lt;em&gt;write a programming language&lt;/em&gt;, the YAML schema wins by a mile and it isn't close. Nobody should build a language to avoid building a config format. That trade is absurd.&lt;/p&gt;
&lt;p&gt;But that wasn't the choice. Lattice already existed. It was not designed for ballistics, has no ballistics features, and would exist unchanged if I'd never written the laboratory. The marginal cost of using it here was zero, and the honest framing is not "I chose Lattice over Python" — it's "I had a language sitting there, and using it was cheaper than inventing a format." If you don't already have one, the answer is almost certainly Lua or Python, and the argument in this post applies to &lt;em&gt;those&lt;/em&gt; just as well. The claim is about embedding a real language versus growing a fake one, not about which real language.&lt;/p&gt;
&lt;p&gt;That said, two things about Lattice specifically turned out to fit better than I expected, and they're worth naming because they're the kind of thing you'd want from any candidate.&lt;/p&gt;
&lt;p&gt;The first is the phase system. Lattice's central idea is that values start &lt;em&gt;fluid&lt;/em&gt; and can be &lt;em&gt;frozen&lt;/em&gt; into immutable &lt;em&gt;crystal&lt;/em&gt; — &lt;a href="https://tinycomputers.io/posts/introducing-lattice-a-crystallization-based-programming-language.html"&gt;it's what the language is about&lt;/a&gt;, and I've written before about &lt;a href="https://tinycomputers.io/posts/crystal-by-reference-making-freeze-mean-it-then-making-it-fast.html"&gt;making that promise actually hold&lt;/a&gt;. An experiment is precisely the kind of value that wants this: you assemble it, you hand it to a solver, and it must not change underneath the solver or between the run and the report that describes the run. In the domain layer that's a declaration, not a discipline:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Projectile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fix&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb nb-Type"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;mass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fix&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;diameter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fix&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fix&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;drag_model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fix&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb nb-Type"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;ballistic_coefficient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fix&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Number&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Every field is &lt;code&gt;fix&lt;/code&gt; — immutable per-field, checked by the language. There are 192 such declarations across the laboratory's domain modules. In a language without that, the equivalent is a frozen dataclass, or a convention, or a code review comment; here it's a property of the type. A config format gets you immutability for free only because it's inert — it can't express computation at all. This gets you both.&lt;/p&gt;
&lt;p&gt;The second is embedding cost. The laboratory ships with a runtime-only binary, &lt;code&gt;clat-run&lt;/code&gt;, that is &lt;strong&gt;655 KB&lt;/strong&gt; on arm64 macOS — the full interpreter with tooling is 1.2 MB. Embedding CPython means shipping CPython; embedding Lua is genuinely small too, which is exactly why Lua keeps winning this category. The point isn't that Lattice is uniquely small, it's that "how much do I have to ship" is a real selection criterion for this kind of tool, and it's one a config format wins by default until you notice you've reimplemented variables.&lt;/p&gt;
&lt;p&gt;So: not a triumph of language design over YAML. A case of the cheapest available option happening to be a real language, and the domain happening to want immutability badly enough that the language's central feature earned its keep.&lt;/p&gt;
&lt;h3&gt;The Boundary I Did Keep&lt;/h3&gt;
&lt;p&gt;There is exactly one place the laboratory refuses to be clever, and it's the interesting design decision in the project.&lt;/p&gt;
&lt;p&gt;The physics does not run in Lattice. It runs in the Rust engine, in a separate process, over a versioned JSON protocol. The laboratory's README is blunt about this: it &lt;em&gt;"does not add a ballistics intrinsic, native extension, or C ABI dependency to the language runtime."&lt;/em&gt; The backend reports itself in every run — &lt;code&gt;backend: process-json-v1&lt;/code&gt;, &lt;code&gt;engine: 0.30.1&lt;/code&gt;, &lt;code&gt;schema: 1&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The obvious alternative was to bind the engine natively. I have the design document for it: a Rust extension at &lt;code&gt;extensions/ballistics/&lt;/code&gt; that statically links the crate and binds its Rust API. It's a real design, and it rejects the naive version for a good reason — the engine's C FFI takes a 26-field flat input struct, and mirroring that by hand through a generic FFI layer means keeping two definitions in sync forever, with a segfault waiting at the end of any drift. The document has been sitting at "Proposed. Architecture decided; pre-implementation" for a while now.&lt;/p&gt;
&lt;p&gt;The process backend shipped instead, and the longer it runs the more I think the boring choice was right. A separate process gives you a version number you can print, a schema you can check, a crash that doesn't take the REPL with it, and an upgrade path where the engine and the language move independently. It also gives you an honest security boundary: the laboratory doesn't start the configured executable until you type &lt;code&gt;:solve&lt;/code&gt;, and the launcher probes candidate engines with a non-solving capability check first.&lt;/p&gt;
&lt;p&gt;The cost is process spawn per solve and JSON on the wire. For an interactive laboratory where a human types between solves, that cost is invisible. If I were solving in a tight loop, I'd be having a different conversation — and that's precisely when the extension design comes off the shelf.&lt;/p&gt;
&lt;p&gt;There's a general principle hiding here. "Bind it natively" and "shell out to it" are usually presented as a performance question. They're mostly a &lt;em&gt;coupling&lt;/em&gt; question, and the coupling bill arrives later than the performance bill does.&lt;/p&gt;
&lt;h3&gt;What Honesty Looks Like in the Output&lt;/h3&gt;
&lt;p&gt;The other thing that falls out of a real language: the tool can afford to tell you what it assumed.&lt;/p&gt;
&lt;p&gt;Every solve reports its defaults explicitly, with JSON paths into the request:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;Assumptions
  &lt;span class="k"&gt;-&lt;/span&gt; default_applied: Aim azimuth defaulted to 0 rad. [$.shot.aim_azimuth_rad]
  &lt;span class="k"&gt;-&lt;/span&gt; default_applied: Shot azimuth defaulted to 0 rad (north). [$.shot.shot_azimuth_rad]
  &lt;span class="k"&gt;-&lt;/span&gt; default_applied: Shooting angle defaulted to 0 rad. [$.shot.shooting_angle_rad]
  &lt;span class="k"&gt;-&lt;/span&gt; default_applied: Cant angle defaulted to 0 rad. [$.shot.cant_angle_rad]
  &lt;span class="k"&gt;-&lt;/span&gt; default_applied: Target height defaulted to 0 m above the local ground datum. [$.shot.target_height_m]
  &lt;span class="k"&gt;-&lt;/span&gt; default_applied: Solver time step defaulted to 0.001 s. [$.solver.time_step_s]
  &lt;span class="k"&gt;-&lt;/span&gt; default_applied: Magnus effect defaulted to disabled. [$.effects.magnus]
  &lt;span class="k"&gt;-&lt;/span&gt; default_applied: Enhanced spin drift defaulted to disabled. [$.effects.enhanced_spin_drift]
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is the sort of thing a config file makes &lt;em&gt;harder&lt;/em&gt;, not easier. In a YAML world, an unspecified field is silently the default, and the only way to know what the default was is to read the source. Here, "I didn't specify a cant angle" and "the solver assumed zero cant" are two different facts and both are visible. Note the last two lines especially: Magnus effect and enhanced spin drift are &lt;em&gt;off&lt;/em&gt;, and the tool says so rather than letting you assume a more sophisticated model than you got.&lt;/p&gt;
&lt;p&gt;Runs also carry a &lt;code&gt;verified: no&lt;/code&gt; flag, distinguishing "the solver produced numbers" from "the numbers were checked against a reference." A calculator that can't express that distinction encourages you to forget it exists.&lt;/p&gt;
&lt;h3&gt;Where It Actually Bit Me&lt;/h3&gt;
&lt;p&gt;I want to be honest about the failure mode of this approach, because I hit it while writing this post.&lt;/p&gt;
&lt;p&gt;I ran the laboratory to capture the transcripts above, and &lt;code&gt;:solve&lt;/code&gt; failed:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;Error&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;process_error&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;undefined&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;variable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'exec_argv'&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;exec_argv&lt;/code&gt; is the builtin that spawns the engine. My first read was that I'd shipped a real bug — that the process backend needed a function the bytecode VMs didn't implement, and the laboratory had only ever been tested on the tree-walking interpreter. That's a plausible bug, and it's the kind of bug that a two-implementation language invites.&lt;/p&gt;
&lt;p&gt;It wasn't that. The launcher had resolved &lt;code&gt;clat&lt;/code&gt; from my &lt;code&gt;PATH&lt;/code&gt; and found an old v0.3.28 build sitting in Homebrew, from long before the VMs got that builtin. My local checkout's binary had been deleted by a sanitizer build earlier in the day, so the fallback kicked in. The banner had been telling me the whole time — &lt;code&gt;Ballistics laboratory — Lattice v0.3.28&lt;/code&gt; — and I hadn't read it.&lt;/p&gt;
&lt;p&gt;Two lessons, both cheap in hindsight. First: print the version of every component, at startup, always. This tool does, which is the only reason the diagnosis took minutes instead of an evening. Second, and more pointed: a launcher that searches &lt;code&gt;PATH&lt;/code&gt; for its own interpreter will eventually find the wrong one. Deterministic discovery order is documented in the README precisely because this is a known hazard, and it still got me, because I was the one who broke the invariant it depends on.&lt;/p&gt;
&lt;p&gt;The nice part is that the failure was &lt;em&gt;loud and local&lt;/em&gt;. The engine ran in another process; the missing builtin failed at the boundary with a specific name in the message. Had I bound the engine natively into the runtime, the equivalent mistake — a stale binary with an ABI that no longer matches — is not an error message. It's a crash, or worse, wrong numbers.&lt;/p&gt;
&lt;h3&gt;What This Buys, Concretely&lt;/h3&gt;
&lt;p&gt;Strip away the ballistics and the claim is small and testable: &lt;strong&gt;if your tool already embeds a scripting language, you should not also invent a configuration language.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;What the laboratory gets for free, by that one decision:&lt;/p&gt;
&lt;p&gt;Variables and reuse, because it's a language. Parameter sweeps are &lt;code&gt;for&lt;/code&gt; loops, not a templating layer. A sweep over three ballistic coefficients isn't three config files and a shell script that concatenates three output files — it's a loop:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;ballistics&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;let&lt;span class="w"&gt; &lt;/span&gt;base&lt;span class="w"&gt; &lt;/span&gt;=&lt;span class="w"&gt; &lt;/span&gt;sessions.experiment_of(lab)
ballistics&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;let&lt;span class="w"&gt; &lt;/span&gt;b&lt;span class="w"&gt; &lt;/span&gt;=&lt;span class="w"&gt; &lt;/span&gt;sessions.backend_of(lab)
ballistics&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;for&lt;span class="w"&gt; &lt;/span&gt;bc&lt;span class="w"&gt; &lt;/span&gt;in&lt;span class="w"&gt; &lt;/span&gt;[0.24,&lt;span class="w"&gt; &lt;/span&gt;0.29,&lt;span class="w"&gt; &lt;/span&gt;0.34]&lt;span class="w"&gt; &lt;/span&gt;{
&lt;span class="w"&gt;              &lt;/span&gt;let&lt;span class="w"&gt; &lt;/span&gt;p&lt;span class="w"&gt; &lt;/span&gt;=&lt;span class="w"&gt; &lt;/span&gt;projectile("bc&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;${&lt;/span&gt;&lt;span class="n"&gt;bc&lt;/span&gt;&lt;span class="cp"&gt;}&lt;/span&gt;",&lt;span class="w"&gt; &lt;/span&gt;gr(175),&lt;span class="w"&gt; &lt;/span&gt;inches(0.308),&lt;span class="w"&gt; &lt;/span&gt;bc,&lt;span class="w"&gt; &lt;/span&gt;"G7",&lt;span class="w"&gt; &lt;/span&gt;inches(1.24))
&lt;span class="w"&gt;              &lt;/span&gt;let&lt;span class="w"&gt; &lt;/span&gt;e&lt;span class="w"&gt; &lt;/span&gt;=&lt;span class="w"&gt; &lt;/span&gt;experiment("bc&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;${&lt;/span&gt;&lt;span class="n"&gt;bc&lt;/span&gt;&lt;span class="cp"&gt;}&lt;/span&gt;",&lt;span class="w"&gt; &lt;/span&gt;p,&lt;span class="w"&gt; &lt;/span&gt;base.rifle,&lt;span class="w"&gt; &lt;/span&gt;base.atmosphere,&lt;span class="w"&gt; &lt;/span&gt;base.winds,&lt;span class="w"&gt; &lt;/span&gt;base.shot,&lt;span class="w"&gt; &lt;/span&gt;base.solver)
&lt;span class="w"&gt;              &lt;/span&gt;let&lt;span class="w"&gt; &lt;/span&gt;r&lt;span class="w"&gt; &lt;/span&gt;=&lt;span class="w"&gt; &lt;/span&gt;backends.solve_with(b,&lt;span class="w"&gt; &lt;/span&gt;e)
&lt;span class="w"&gt;              &lt;/span&gt;print("&lt;span class="cp"&gt;${&lt;/span&gt;&lt;span class="n"&gt;bc&lt;/span&gt;&lt;span class="cp"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;ok=&lt;span class="cp"&gt;${&lt;/span&gt;&lt;span class="n"&gt;backends&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;solve_succeeded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="cp"&gt;}&lt;/span&gt;")
&lt;span class="w"&gt;            &lt;/span&gt;}
0.24&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;ok=true
0.29&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;ok=true
0.34&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;ok=true
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Three experiments derived from one base value, three solves, three results — as values in hand, which the analysis module can then interrogate. Worth being precise about the boundary: &lt;code&gt;solve_with&lt;/code&gt; hands results back to &lt;em&gt;you&lt;/em&gt;. It doesn't file them into the session's run history, which is what the colon commands manage; &lt;code&gt;:solve&lt;/code&gt; and a programmatic solve are two different doors, and only the first one updates &lt;code&gt;:compare&lt;/code&gt;'s notion of "previous run." That's a seam, and an honest one to point at.&lt;/p&gt;
&lt;p&gt;Comparison between the session's own runs is a session feature rather than a diffing tool over output files, because both runs are values the session is holding. Solve the 175 grain load, swap in the 230, solve again, and &lt;code&gt;:compare&lt;/code&gt; interleaves them with per-row deltas:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;ballistics&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;compare&lt;/span&gt;
&lt;span class="n"&gt;Trajectory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;comparison&lt;/span&gt;
&lt;span class="n"&gt;Series&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Trajectory&lt;/span&gt;&lt;span class="w"&gt;                  &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Distance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;yd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Velocity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ft&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Energy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ft&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;lb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Delta&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;velocity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ft&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Delta&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;energy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ft&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;lb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;-------+-----------------------------+---------------+-----------------+----------------+-----------------------+---------------------&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mf"&gt;0.00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;175&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;match&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;yards&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="mf"&gt;20.00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="mf"&gt;2662.10&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="mf"&gt;2753.29&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;                 &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mf"&gt;0.00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mf"&gt;0.00&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mf"&gt;1.00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;230&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;high&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;BC&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;load&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="mf"&gt;20.00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="mf"&gt;2674.94&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="mf"&gt;3653.61&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mf"&gt;12.84&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mf"&gt;900.32&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mf"&gt;0.00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;175&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;match&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;yards&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="mf"&gt;40.00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="mf"&gt;2624.49&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="mf"&gt;2676.05&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;                 &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mf"&gt;0.00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mf"&gt;0.00&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mf"&gt;1.00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;230&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;high&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;BC&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;load&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="mf"&gt;40.00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="mf"&gt;2650.01&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="mf"&gt;3585.82&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mf"&gt;25.52&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mf"&gt;909.77&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;(Abridged — the real table runs every ten yards to the full range and carries drop, windage, Mach and time columns too. Series &lt;code&gt;0.00&lt;/code&gt; is the baseline; deltas are each series minus it.)&lt;/p&gt;
&lt;p&gt;Persistence is &lt;code&gt;:save&lt;/code&gt; and &lt;code&gt;:load&lt;/code&gt; over a JSON artifact, so the &lt;em&gt;serialized&lt;/em&gt; form exists — but it's an artifact of a session, not the authoring interface. Nobody hand-writes it:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;ballistics&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;save&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/tmp/rt2.json"&lt;/span&gt;
&lt;span class="n"&gt;Saved&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;experiment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;rt2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="n"&gt;ballistics&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sessions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace_experiment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lab&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;experiment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"DECOY load"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;decoy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;ballistics&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;show&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;experiment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DECOY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;load&lt;/span&gt;
&lt;span class="n"&gt;ballistics&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;load&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/tmp/rt2.json"&lt;/span&gt;
&lt;span class="n"&gt;ballistics&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;show&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;experiment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;230&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;high&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;BC&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;load&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;One detail worth knowing if you use this: &lt;code&gt;:reset&lt;/code&gt; clears &lt;em&gt;run state&lt;/em&gt;, not the experiment. After a reset the loaded experiment is still sitting there; what's gone is the solved-run history that &lt;code&gt;:compare&lt;/code&gt; reads. It's the right split — you rarely want to retype an experiment just because you want to discard its runs — but the command name doesn't tell you that, and I only established it by resetting and then checking what survived.&lt;/p&gt;
&lt;p&gt;And the failure modes get better. A typo in a config file is a schema violation at load time, at best. A typo at this prompt is an evaluation error with a line, immediately, in a session that stays alive so you can retype the line. Parse, evaluation, command, and engine failures are all reported without ending the session — which sounds like a small quality-of-life detail until you're forty lines into building an experiment.&lt;/p&gt;
&lt;p&gt;The honest limitations, in the same spirit: the v1 REPL uses a portable &lt;code&gt;input()&lt;/code&gt; loop, so there's no line editing, no history, no completion. It isn't available in WASM builds, because terminal input and process execution aren't. Those are real gaps, and they're the price of the portable path.&lt;/p&gt;
&lt;h3&gt;The Thing I Keep Relearning&lt;/h3&gt;
&lt;p&gt;I've built a fair number of tools that grew a configuration format, and the format was never the interesting part of any of them. It was always overhead — a parser, a schema, a validation layer, a units convention, a documentation page, and a slow drift toward Turing completeness that nobody planned and everybody regretted.&lt;/p&gt;
&lt;p&gt;The ballistics laboratory has none of that, and it isn't because I was clever. It's because the language was already sitting there. The REPL is Lattice, so the experiment is a Lattice value, so configuring the experiment is writing Lattice, so there was never a moment where inventing a config format seemed necessary.&lt;/p&gt;
&lt;p&gt;If your project has an embedded language and a config format, it's worth asking what the config format is actually buying you. Sometimes the answer is a real one — a stable serialization boundary, a format non-programmers can edit, a thing you can diff in review. But often the answer is that the config format came first, the scripting language came later, and nobody went back to ask whether both were still needed.&lt;/p&gt;
&lt;p&gt;The laboratory ships in the Lattice repository under &lt;code&gt;examples/ballistics_lab/&lt;/code&gt;, as of v0.6.4. It's an example in the literal sense: not a product, but a demonstration that a serious domain tool can be built in the language without escaping to C — which was the actual thing I wanted to find out.&lt;/p&gt;</description><category>ballistics</category><category>domain-specific languages</category><category>external ballistics</category><category>language design</category><category>lattice</category><category>process isolation</category><category>programming languages</category><category>repl</category><category>software architecture</category><category>tooling</category><guid>https://tinycomputers.io/posts/the-config-language-that-was-already-there.html</guid><pubDate>Sun, 02 Aug 2026 17:00:00 GMT</pubDate></item></channel></rss>