<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>The Zen of Coding</title>
    <link>https://joel.id</link>
    <description>Finding flow</description>
    <language>en</language>
    <atom:link href="https://joel.id/blog/rss.xml" rel="self" type="application/rss+xml"/><lastBuildDate>Thu, 18 Dec 2025 11:32:35 +0000</lastBuildDate>
    <item>
      <title>Hacking LLDB for a great Zig debugging experience</title>
      <link>https://joel.id/hacking-lldb-for-a-great-zig-debugging-experience/</link>
      <guid isPermaLink="true">https://joel.id/hacking-lldb-for-a-great-zig-debugging-experience/</guid>
      <pubDate>Thu, 18 Dec 2025 08:43:35 +0000</pubDate>
      <dc:creator>Joel Reymont</dc:creator><description><![CDATA[<p>Discuss it on <a href="https://x.com/joelreymont/status/2001574396096340146?s=20" rel="noreferrer">Twitter</a>, <a href="https://www.reddit.com/r/Zig/comments/1ppmanm/hacking_lldb_for_a_great_zig_debugging_experience/" rel="noreferrer">Reddit</a> or <a href="https://news.ycombinator.com/item?id=46310523" rel="noreferrer">Hacker News</a>.</p><p>Zig is better than C. And that’s great — until you try to debug it.</p><p>If you’ve ever dropped into LLDB while debugging Zig, you already know the pain: slices look like random structs, optionals are unreadable, error unions feel hostile, and expressions like <code>slice[0]</code> just… don’t work.</p><p>This post introduces <a href="https://github.com/joelreymont/zdb"><strong>zdb</strong></a>, an LLDB plugin that makes debugging Zig feel <em>normal</em> — without rebuilding LLDB or messing with Python scripts.</p><h2 id="the-problem">The Problem</h2><p>Here’s a simple Zig program (from <code>test/test_types.zig</code> in the <a href="https://github.com/joelreymont/zdb" rel="noreferrer">zdb repo</a>):</p><pre><code class="language-zig">const std = @import("std");

const Color = enum { red, green, blue, yellow };

const TestStruct = struct {
    name: []const u8,
    values: []i32,
    optional_value: ?i32,
    error_result: MyError!i32,
    shape: Shape,
};

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    // Test slices
    const string_slice: []const u8 = "Hello, zdb debugger!";
    const int_slice: []const i32 = &amp;[_]i32{ 1, 2, 3, 4, 5 };

    // Test optionals
    const some_value: ?i32 = 42;

    // Test std library
    var list: std.ArrayListUnmanaged(i32) = .empty;
    try list.appendSlice(allocator, &amp;.{ 10, 20, 30 });

    std.debug.print("Breakpoint here!\n", .{});  // breakpoint here
}
</code></pre><p>Now stop at the breakpoint and inspect things in <em>stock</em> LLDB:</p><pre><code>(lldb) frame variable string_slice int_slice color test_struct list
([]u8) string_slice = (ptr = "Hello, zdb debugger!", len = 20)
([]i32) int_slice = {
  ptr = 0x00000001000da244
  len = 5
}
(test_types.Color) color = blue
(test_types.TestStruct) test_struct = {
  name = (ptr = "test object", len = 11)
  values = {
    ptr = 0x000000016fdfe588
    len = 3
  }
  optional_value = (data = 42, some = '\x01')
  error_result = (value = 100, tag = 0)
  shape = {
    payload = { ... }
    tag = circle
  }
}
(array_list.Aligned(i32,null)) list = {
  items = {
    ptr = 0x0000000100180000
    len = 3
  }
  capacity = 32
}
</code></pre><p>This is <em>technically</em> correct — but not very useful.</p><p>And if you try to write Zig-like expressions:</p><pre><code>(lldb) p int_slice[0]
                  ^
                  error: type '[]i32' does not provide a subscript operator
</code></pre><p>LLDB has no idea that a slice is really <code>(ptr, len)</code>. It just sees a struct.</p><h2 id="existing-solutions-and-why-they%E2%80%99re-not-great">Existing Solutions (And Why They’re Not Great)</h2><p>LLDB lets you register Python-based type summaries:</p><pre><code class="language-python"># zig_formatters.py
import lldb

def slice_summary(valobj, internal_dict):
    ptr = valobj.GetChildMemberWithName('ptr')
    length = valobj.GetChildMemberWithName('len').GetValueAsUnsigned()
    return f'len={length} ptr={ptr.GetValue()}'

def __lldb_init_module(debugger, internal_dict):
    debugger.HandleCommand(
        'type summary add -F zig_formatters.slice_summary -x "\\[\\].*"'
    )
    print('[python] Registered slice formatter')
</code></pre><p>That gets you nicer output:</p><pre><code>(lldb) frame variable string_slice int_slice
([]u8) string_slice = len=20 ptr=0x00000001000e3fde
([]i32) int_slice = len=5 ptr=0x00000001000da244
</code></pre><p>But expressions still don’t work:</p><pre><code>(lldb) p int_slice[0]
error: type '[]i32' does not provide a subscript operator
</code></pre><p><strong>Problems:</strong></p><ul><li>You’re writing Python to debug systems code</li><li>Crossing Python ↔ C++ boundaries is slow</li><li>No expression support</li><li>No string content display</li></ul><h3 id="zig-lldb-the-nuclear-option">zig-lldb (The Nuclear Option)</h3><p>Jacob Young’s <a href="https://github.com/jacobly0/llvm-project/tree/lldb-zig"><strong>zig-lldb</strong></a> fork adds a full <code>TypeSystemZig</code> to LLDB.</p><p>It’s <em>excellent</em>. It’s also a full LLDB fork.</p><p><strong>Upsides</strong></p><ul><li>Real Zig semantics</li><li>Expressions work</li><li>Variables view works</li></ul><p><strong>Downsides</strong></p><ul><li>Rebuild LLDB (bring snacks!)</li><li>Track upstream changes forever</li><li>Not usable with system or Homebrew LLDB</li></ul><h2 id="enter-zdb">Enter zdb</h2><p>zdb takes a different approach: <em>how far can we go without rebuilding LLDB?</em></p><p>zdb is a <strong>native C++ LLDB plugin</strong> that:</p><ol><li>Registers real type formatters (no Python)</li><li>Rewrites expressions so Zig syntax works</li><li>Runs on stock LLDB</li></ol><p>Load it:</p><pre><code>(lldb) plugin load libzdb.dylib
[zdb] Loaded 19 formatters + expression syntax
</code></pre><p>Now things look… sane:</p><pre><code>(lldb) frame variable string_slice int_slice
([]u8) string_slice = "Hello, zdb debugger!" {
  ptr = 0x00000001000e3fde "Hello, zdb debugger!"
  len = 20
}
([]i32) int_slice = len=5 ptr=0x1000da244 {
  ptr = 0x00000001000da244
  len = 5
}
</code></pre><p>And expressions finally behave:</p><pre><code>(lldb) p int_slice[0]
(int) $0 = 1

(lldb) p list[0]
(int) $1 = 10

(lldb) p test_struct.optional_value.?
(int) $2 = 42

(lldb) p test_struct.error_result catch 0
(int) $3 = 100
</code></pre><p>That alone makes day-to-day debugging dramatically better.</p><h2 id="how-this-works-aka-the-dark-arts">How This Works (a.k.a. The Dark Arts)</h2><p>LLDB has two APIs:</p><ul><li><strong>Public API</strong> (<code>SBTarget</code>, <code>SBValue</code>, etc.)</li><li><strong>Internal API</strong> (<code>TypeCategoryImpl</code>, <code>FormatManager</code>, etc.)</li></ul><p>The public API is… polite but limited.</p><p>The internal API does the real work — and isn’t exported.</p><p>So zdb cheats!</p><h3 id="step-1-find-internal-symbols">Step 1: Find Internal Symbols</h3><p>They’re not exported, but they’re <em>there</em>:</p><pre><code class="language-bash">nm -C liblldb.dylib | grep AddTypeSummary
0000000000360f38 t lldb_private::TypeCategoryImpl::AddTypeSummary(...)
</code></pre><h3 id="step-2-compute-the-base-address">Step 2: Compute the Base Address</h3><p>We anchor off a known exported symbol:</p><pre><code class="language-cpp">void* ref = dlsym(handle, "_ZN4lldb10SBDebugger10InitializeEv");
uintptr_t base = (uintptr_t)ref - reference_offset;
</code></pre><p>Now every internal symbol is just <code>base + offset</code>.</p><h3 id="step-3-call-it-with-the-right-abi">Step 3: Call It with the Right ABI</h3><p>ARM64 calling conventions matter. <code>shared_ptr</code> gets passed <em>indirectly</em>:</p><pre><code class="language-cpp">using AddTypeSummaryFn = void (*) (
    void* this_ptr,
    const char* name_ptr,
    size_t name_len,
    int match_type,
    SharedPtrLayout* sp
);
</code></pre><p>Mess this up and you get instant crashes.</p><h3 id="step-4-rewrite-expressions">Step 4: Rewrite Expressions</h3><p>zdb intercepts expressions and rewrites them:</p>

<table>
<thead>
<tr>
<th>Zig</th>
<th>Becomes</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>slice[n]</code></td>
<td><code>slice.ptr[n]</code></td>
</tr>
<tr>
<td><code>list[n]</code></td>
<td><code>list.items.ptr[n]</code></td>
</tr>
<tr>
<td><code>opt.?</code></td>
<td><code>opt.data</code></td>
</tr>
<tr>
<td><code>err catch x</code></td>
<td><code>(err.tag == 0 ? err.value : x)</code></td>
</tr>
</tbody>
</table>

<p>Users never see this. They just type Zig.</p><h2 id="offset-tables">Offset Tables</h2><p>Offsets change between LLDB versions, so zdb uses JSON tables:</p><pre><code class="language-json">{
  "version": "21.1.7",
  "reference_symbol": "_ZN4lldb10SBDebugger10InitializeEv",
  "reference_offset": "0x4c670",
  "symbols": {
    "TypeCategoryImpl::AddTypeSummary": { "offset": "0x35ff38" }
  }
}
</code></pre><p>There’s a script to generate these automatically:</p><pre><code class="language-bash">python3 tools/dump_offsets.py liblldb.dylib &gt; lldb-21.1.7.json
</code></pre><h2 id="limitations-yes-there-are-some">Limitations (Yes, There Are Some)</h2><p>The big missing piece is <strong>Variables View expansion</strong>.</p><p>That requires registering C++ synthetic children via <code>std::function</code>. Unfortunately, ABI mismatches between plugin and LLDB builds make this extremely fragile.</p><p>So:</p><ul><li>CLI expressions work</li><li>GUI tree expansion does not</li></ul><p>For most Zig debugging, this turns out to be fine.</p><h2 id="comparison">Comparison</h2>

<table>
<thead>
<tr>
<th>Feature</th>
<th>Stock LLDB</th>
<th>Python</th>
<th>zig-lldb</th>
<th>zdb</th>
</tr>
</thead>
<tbody>
<tr>
<td>Install</td>
<td>Built-in</td>
<td>Script</td>
<td>Rebuild</td>
<td>Plugin</td>
</tr>
<tr>
<td>Summaries</td>
<td>✗</td>
<td>✓</td>
<td>✓</td>
<td>✓</td>
</tr>
<tr>
<td><code>slice[n]</code></td>
<td>✗</td>
<td>✗</td>
<td>✓</td>
<td>✓</td>
</tr>
<tr>
<td>Optionals</td>
<td>✗</td>
<td>✗</td>
<td>✓</td>
<td>✓</td>
</tr>
<tr>
<td>Variables view</td>
<td>✗</td>
<td>Partial</td>
<td>✓</td>
<td>✗</td>
</tr>
<tr>
<td>Performance</td>
<td>—</td>
<td>Slow</td>
<td>Fast</td>
<td>Fast</td>
</tr>
</tbody>
</table>

<h2 id="final-thoughts">Final Thoughts</h2><p>zdb is intentionally pragmatic.</p><p>It’s not as pure as zig-lldb, and it’s definitely more cursed than Python formatters — but it hits a sweet spot:</p><ul><li>Works with stock LLDB</li><li>Makes Zig readable</li><li>Makes expressions usable</li></ul><p>For everyday Zig debugging, that’s usually all you want.</p><p>Your contributions are welcome — especially offset tables for new LLDB releases!</p><p><em>Thanks to Jacob Young for </em><a href="https://github.com/jacobly0/llvm-project/tree/lldb-zig" rel="noreferrer"><em>zig-lldb</em></a><em>, which proved that proper Zig debugging support is possible and inspired this lighter-weight alternative.</em></p>
]]></description>
    </item>
    <item>
      <title>AI will write your next compiler!</title>
      <link>https://joel.id/ai-will-write-your-next-compiler/</link>
      <guid isPermaLink="true">https://joel.id/ai-will-write-your-next-compiler/</guid>
      <pubDate>Wed, 26 Nov 2025 13:50:59 +0000</pubDate>
      <dc:creator>Joel Reymont</dc:creator><description><![CDATA[<p>Discuss on <a href="https://news.ycombinator.com/item?id=46057430" rel="noreferrer">Hacker News</a>, <a href="https://x.com/joelreymont/status/1993679163291496574?s=20" rel="noreferrer">Twitter</a> or <a href="https://www.reddit.com/r/programming/comments/1p78a8h/ai_will_write_your_next_compiler/" rel="noreferrer">Reddit</a>.  Also, <a href="https://joel.id/resume" rel="noreferrer">consider hiring me</a>!</p><h3 id="it-was-a-dark-and-stormy-night">It was a dark and stormy night</h3><p>Well, a gloomy Kyiv morning.</p><p>I opened my laptop and discovered that my Claude Code account had been <em>disabled</em>. Not only that — I’d been refunded the unused portion of my $200/month Max plan and downgraded to the Free tier. I had 10–15 active Claude Code Web sessions working across multiple open-source projects, and everything ground to a halt.</p><p>Sensing impending catastrophe, I navigated to the billing page and paid $200 again… only to watch it refunded a few minutes later.</p><h3 id="how-it-started">How it started</h3><p>A couple of weeks earlier, I suddenly found myself with $1000 in Claude Web credits and just one week to burn them. I decided to use the free money for the greater good: push AI as far as it could and try to improve existing open-source projects.</p><p>So I started hacking.</p><p>I tried adding <a href="https://github.com/ocaml/ocaml/pull/14353">REPL history navigation and editing</a> to <a href="https://ocaml.org/manual/5.4/debugger.html">ocamldebug</a>, the OCaml bytecode debugger. It went surprisingly well — except I missed that line-based breakpoints exist already.</p><p>Then I tried adding <a href="https://joel.id/native-binary-debugging-for-ocaml/">native binary debugging to OCaml</a>. That also worked. Yes, my implementation is fully working. Yes, it’s tested!</p><p>Yes, <a href="https://github.com/ocaml/ocaml/pull/14369">dumping a huge PR on unsuspecting OCaml maintainers</a> was a stunt and I regret it now. I also apologize for burdening the OCaml maintainers with it!</p><p>I should have started a discussion or <a href="https://github.com/joelreymont/zig/pull/1">opened a tiny PR against your own repo first</a> to validate the idea, like I did with Zig.</p><p>Except that experiment didn’t go well either.</p><p>I spent two weeks building a non-LLVM ARM64 backend for Zig… only to discover such a backend already existed. Then I found a small compiler bug, wrote a minimal patch, included detailed examples—and got slapped down because Zig has a strict <em>no-LLM contributions</em> policy.</p><p>The moral: <a href="https://joel.id/build-your-dreams/">go play in your own playground</a>!</p><h3 id="growing-compilers">Growing compilers</h3><p>Some insist that AI “steals” rather than creates. For example, someone claimed my DWARF debugging support was copied from OxCaml. I <a href="https://github.com/ocaml/ocaml/pull/14369#issuecomment-3556624486">disagree</a> — and I think the premise doesn’t matter. Humans copy each other’s code constantly. Writers read books to write new books. Training on existing code is no different.</p><p>AI <em>can</em> write a compiler using the knowledge it’s trained on.</p><p>I’m putting money where my mouth is: I’m working on two such projects now:</p><ul><li>A Common Lisp compiler targeting embedded and bare-metal environments.</li><li>A source-to-source compiler from Ghidra’s <a href="https://ghidra.re/ghidra_docs/languages/html/sleigh.html">Sleigh</a> to Rust plugins for <a href="https://binary.ninja">Binary Ninja</a>.</li></ul><p>So what does it look like when a model writes a Lisp compiler?</p><p>Pretty damn sentient.</p><p>It figured out that it’s missing <code>labels</code> a feature of the language and that it needs to implement them.</p><pre><code>⏺ I can confirm that labels is not currently supported in the bootstrap compiler.
   The compiler itself uses labels heavily (because it runs on SBCL), but when
  compiling Habu source code, labels is not handled. I need to implement labels
  support.

  Let me first understand what forms are already supported to see what patterns I
   can follow. The closest analogs are LET/LET* and lambda/funcall. Let me look
  at how defun is handled to understand the function environment:
</code></pre><p>It knows how to search through existing code.</p><pre><code>⏺ Search(pattern: "defun", path: "bootstrap/compiler.lisp", output_mode: "content")
  ⎿ &nbsp;Found 100 lines (ctrl+o to expand)

⏺ Let me look at the compile-forms function to understand how defun is handled at
   the top level:

⏺ Search(pattern: "nc-compile-forms", path: "bootstrap/compiler.lisp", output_mode:
        "content")
  ⎿ &nbsp;Found 100 lines (ctrl+o to expand)
</code></pre><p>And it knows how to put two and two together, e.g. that labels are local recursive functions.</p><pre><code>⏺ Now I understand the structure. Let me look at how lambda captures free
  variables and how funcall works, since labels is essentially local recursive
  functions:

⏺ Read(bootstrap/compiler.lisp)
  ⎿ &nbsp;Read 100 lines
</code></pre><p>And here’s the kicker: it knows how to simplify labels transform them into something it has already implemented!</p><pre><code>⏺ I see how lambda and funcall work. For labels, I can transform it into nested
  lambdas with mutual recursion. The key insight is:

  (labels ((fn1 (args...) body1)
           (fn2 (args...) body2))
    main-body)

  Can be transformed into letrec-style bindings using closures. The standard
  approach is to use a Y-combinator-like technique or to use mutation (setq) to
  tie the knot.

  Let me implement labels by transforming it into a let/setq pattern that
  establishes the recursive bindings:

  (labels ((fact (n) (if (= n 0) 1 (* n (fact (- n 1))))))
    (fact 5))

  becomes:

  (let ((fact nil))
    (setq fact (lambda (n) (if (= n 0) 1 (* n (funcall fact (- n 1))))))
    (funcall fact 5))
</code></pre><h3 id="where-this-is-going">Where this is going</h3><p>My prediction: <strong>AI will write your next compiler.</strong></p><p>Existing projects will struggle to accept AI contributions — not because AI is incapable, but because our development process isn’t ready for it. Reviewing large AI-generated code changes is uncomfortable, foreign, and time-consuming.</p><p>But a brand-new compiler? A greenfield project?<br>AI will thrive there.</p><p>Working with AI today is more <em>art</em> than science. You have to guide it, correct it, and force it to confront its own design decisions.</p><ul><li>AI: My tests are slow because I’m rebuilding the whole stdlib` all the time.</li><li>Me: Why are you rebuilding it all the time?</li><li>AI: Because I have no caching of MIR, etc. code and no incremental compilation. And because I’m rebuilding <code>compiler_rt </code>for every test.</li><li>Me: Well, then focus on caching of build artifacts and incremental completion to speed up your damn tests!</li></ul><h3 id="a-glimpse-of-what%E2%80%99s-coming">A glimpse of what’s coming</h3><p>Eventually, we’ll build the infrastructure to let AI write code without human micromanagement. You review assembly emitted by your existing compiler — you run it, test it, benchmark it, and trust the toolchain.</p><p>AI-written code will be no different.</p><p>If it:</p><ul><li>meets your requirements</li><li>passes your test suite</li><li>introduces no regressions</li><li>performs within expected bounds</li></ul><p>…why would you care how it made the sausage?</p><p><strong>P.S.</strong> If you enjoyed this, you may also like:<br>👉 <a href="https://joel.id/build-your-dreams/"><em>Build Your Dreams</em></a></p>
]]></description>
    </item>
    <item>
      <title>Build your dreams!</title>
      <link>https://joel.id/build-your-dreams/</link>
      <guid isPermaLink="true">https://joel.id/build-your-dreams/</guid>
      <pubDate>Tue, 25 Nov 2025 09:30:32 +0000</pubDate>
      <dc:creator>Joel Reymont</dc:creator><description><![CDATA[<p>Discuss on <a href="https://news.ycombinator.com/item?id=46044056" rel="noreferrer">Hacker News</a>, <a href="https://x.com/joelreymont/status/1993251055484973313?s=20" rel="noreferrer">Twitter</a> or <a href="https://www.reddit.com/r/programming/comments/1p687mx/build_your_dreams/" rel="noreferrer">Reddit</a>.  Also, <a href="https://joel.id/resume" rel="noreferrer">consider hiring me</a>!</p><p>You might also enjoy <a href="https://joel.id/ai-will-write-your-next-compiler/" rel="noreferrer">AI will write your next compiler!</a></p><p>I’m 51, and I started coding 37 years ago — writing BASIC on my lap during a 1.5-hour bus ride from <a href="https://maps.app.goo.gl/jthBdk4onRKWoxU77">Havana</a> (where I went to school) back to <a href="https://maps.app.goo.gl/jAfjwni843qkgzQv6">Santa Cruz del Norte</a>. Seats were usually taken, so I coded while sitting on the wheel cover. Not ideal ergonomics, but perfect motivation.</p><p>I got paid to program about four years later — my first $300 for breaking the copyright protection on a medical system. Different country, different time.</p><p>I’ve never touched Fortran or COBOL, but I’ve programmed in almost everything else at least once. And some languages I keep coming back to again and again (I’m looking at you, Forth and Lisp).</p><p>I’ve written trading systems and device drivers. I started a blockchain company and ran it into the ground. I’ve been around long enough to have bought the t-shirt <em>and</em> the sticker pack.</p><p>Have I established my bona fides yet? If not, you can browse <a href="https://joel.id/resume/">my resume</a> and <a href="https://github.com/joelreymont/">open-source work</a>.</p><h2 id="the-craft">The Craft</h2><p>I care about the craft. I’ve spent decades honing it. Recently I wrote a <a href="https://github.com/HexRaysSA/sleigh-compiler?ref=joel.id">transpiler from Ghidra’s Sleigh to Rust/C++</a>. I first attempted it in Smalltalk (don’t ask — aborted), then in Zig, then <a href="https://github.com/joelreymont/bebop-caml">OCaml</a>, then Rust, just to find the most elegant expression. Rust pleasantly surprised me once I gave up on arenas and embraced <code>Rc&lt;RefCell&lt;T&gt;&gt;</code>. And OCaml still rules the compiler-development world.</p><p>Maybe I care too much about the process and not enough about the outcome.</p><h2 id="a-life-of-dreams">A Life of Dreams</h2><p>At 51, in the second half of my life, I want to leave a visible trace — something <em>delivered</em> into the world. I’m self-taught (skipped college), always learning, but delivery matters more than perfect process.</p><p>I’m an average developer living an average life — wartime Kyiv notwithstanding. I have a wife and two daughters and only so many hours each day. Contracting sometimes leaves me with gaps to experiment, but my day jobs rarely give me enough time for the strange hobby projects I dream about.</p><p>And my dreams are definitely off the beaten path:</p><ul><li>Why has the <a href="https://s-graphics.neocities.org/">Nichimen Mirai</a> story never been repeated?</li><li>Why should I pick Rust simply because it has <a href="https://github.com/lalrpop/lalrpop">LALRPOP</a>, while Zig doesn’t?</li><li>Why can’t I run lifelike drone simulations in Unreal Engine but use Julia instead of Python?</li><li>Why does the ML world insist on Python anyway? Why can't I use Julia?</li></ul><p>I want to build a knowledge base that lives <em>on top of</em> the codebase — without littering someone else’s source code with comments.</p><p>I want <a href="https://github.com/ocaml/ocaml/pull/14350">keyboard navigation and history editing in ocamldebug</a>.<br>I want <a href="https://joel.id/native-binary-debugging-for-ocaml/">easier native OCaml debugging on macOS</a>.<br>I want a super-fast non-LLVM Zig backend for ARM64.<br>I want <a href="https://joel.id/julia-my-love/">tiny, deployable Julia binaries</a> for microcontrollers and embedded Linux.</p><p>I love what Naughty Dog and Andy Gavin did with <a href="https://www.reddit.com/r/programming/comments/bm9p5e/goal_is_a_compiled_lisp_dialect_created_by/">GOAL</a>.<br>I want Lisp running on drones with remote hot-patching. But no existing Lisp lets me develop on macOS and deploy to ARM64 embedded or bare-metal targets.</p><h2 id="why-ai-changed-everything">Why AI Changed Everything</h2><p>All of this felt like a pipe dream — until now.</p><p>Nowadays, I’m so excited that I wake up at 4–5 a.m. <a href="https://joel.id/artisanal-coding-is-dead-long-live-artisanal-coding/">just to code using AI</a>. I can go all day, non-stop. Every new model release feels like Christmas morning: <em>What can it do? What can I do with it?</em></p><p>I’m a college drop-out and an autodidact. AI can teach me concepts I thought were too hard to learn. AI can dig into a complex codebase on my behalf, distill it into parts, and tell me how they relate to each other. AI can tell me if my code sucks and how I can improve it. I can <a href="https://x.com/geoffreylitt/status/1991909304085987366?s=20">combine AI with artisanal coding</a> if I have time (Hi <a href="https://x.com/geoffreylitt">Geoffrey Litt</a>!).</p><p>This is the time when any motivated developer can explore languages, compilers, and entire systems for pennies. I realized I can <em>actually build</em> everything I’ve been dreaming about. No permission required. No gatekeepers. Just work.</p><p>The only thing standing between me and the things I want to create is my own <strong>laziness</strong>.</p><p>And the same goes for you.</p><p><strong>Build your dreams.</strong></p><p>And <a href="https://x.com/joelreymont">follow me</a> on Twitter!</p><p><em>P.S. Pushing my ambitions onto unsuspecting open-source communities was a mistake I won’t repeat. The best playground is always your own project.</em></p>
]]></description>
    </item>
    <item>
      <title>Julia, my love!</title>
      <link>https://joel.id/julia-my-love/</link>
      <guid isPermaLink="true">https://joel.id/julia-my-love/</guid>
      <pubDate>Sun, 23 Nov 2025 08:59:01 +0000</pubDate>
      <dc:creator>Joel Reymont</dc:creator><description><![CDATA[<p>Discuss it on <a href="https://news.ycombinator.com/item?id=46021932" rel="noreferrer">Hacker News</a>, <a href="https://x.com/joelreymont/status/1992519576664379529?s=20" rel="noreferrer">Twitter</a>, <a href="https://www.reddit.com/r/Julia/comments/1p4i7yd/building_standalone_julia_binaries_a_complete/" rel="noreferrer">Reddit</a> or the <a href="https://discourse.julialang.org/t/a-complete-guide-to-building-stand-alone-binaries-with-julia-updated-for-1-12/134054" rel="noreferrer">Julia discussion forum</a>.</p><p><a href="https://joel.id/build-your-dreams/" rel="noreferrer">Build your dreams</a> and <a href="https://joel.id/resume" rel="noreferrer">consider hiring me</a>! </p><p>You might also enjoy <a href="https://joel.id/ai-will-write-your-next-compiler/" rel="noreferrer">AI will write your next compiler!</a></p><h1 id="a-complete-guide-to-building-standalone-julia-binaries">A complete guide to building Standalone Julia Binaries</h1><h2 id="table-of-contents">Table of Contents</h2><ul><li><a>Summary</a></li><li><a>Why Standalone Julia Binaries Matter</a></li><li><a>The Evolution: Stock vs. Enhanced StaticCompiler.jl</a></li><li><a>Let's Build Something: Hello World to Production</a></li><li><a>Binary Size Optimization</a></li><li><a>Real-World Example: Statistics Library</a></li><li><a>Performance Comparison</a></li><li><a>Deployment Scenarios</a></li><li><a>Conclusion: The Best of All Worlds</a></li><li><a>Final Thoughts</a></li></ul><p>This article explains how to compile Julia code into standalone native executables and shared libraries using <a href="https://github.com/tshort/StaticCompiler.jl/">StaticCompiler.jl</a>.</p><p>It covers verification, size and performance optimization, cross-language integration with C/C++ and Python, package-level compilation, and deployment scenarios such as embedded systems, HPC clusters, and commercial applications.</p><h2 id="why-standalone-julia-binaries-matter">Why Standalone Julia Binaries Matter</h2><p>Julia has revolutionized scientific computing with its "looks like Python, runs like C" promise. But there's always been one challenge: <strong>deployment</strong>.</p><p>Traditional Julia programs require users to:</p><ul><li>Install the Julia runtime (150+ MB)</li><li>Manage package dependencies</li><li>Deal with pre-compilation delays</li><li>Navigate environment setup</li></ul><p>This works great for development and research, but creates friction for production deployment, especially in these scenarios:</p><h3 id="embedded-systems-iot"><strong>Embedded Systems &amp; IoT</strong></h3><p>Deploying to microcontrollers, Raspberry Pi, or edge devices where:</p><ul><li>Storage is limited (KB, not GB)</li><li>No package manager available</li><li>Fast startup is critical</li><li>Users can't install Julia</li></ul><h3 id="high-performance-computing"><strong>High-Performance Computing</strong></h3><p>Supercomputers and clusters where:</p><ul><li>Binaries need to be self-contained</li><li>Consistent performance is crucial</li><li>Integration with C/Fortran code is common</li><li>Job schedulers expect executables</li></ul><h3 id="commercial-software-distribution"><strong>Commercial Software Distribution</strong></h3><p>Shipping products to customers who:</p><ul><li>Don't have Julia installed</li><li>Shouldn't see your source code</li><li>Expect "just works" executables</li><li>Need C/C++ integration</li></ul><h3 id="cross-language-integration"><strong>Cross-Language Integration</strong></h3><p>Calling Julia from:</p><ul><li>C/C++ applications</li><li>Python (via ctypes/cffi)</li><li>Rust programs</li><li>Legacy systems</li></ul><p>This is where <strong>StaticCompiler.jl</strong> comes in: it compiles Julia code to standalone native executables and shared libraries, with no Julia runtime required.</p><h2 id="the-evolution-stock-vs-enhanced-staticcompilerjl">The Evolution: Stock vs. Enhanced StaticCompiler.jl</h2><p><a href="https://github.com/tshort/StaticCompiler.jl/">StaticCompiler.jl</a> has always been capable of creating standalone binaries. But like any powerful tool, using it effectively required significant expertise. The <a href="https://github.com/tshort/StaticCompiler.jl/pull/180">updated version</a> we'll explore doesn't change the fundamental compilation—it adds <strong>intelligence, automation, and guidance</strong> to make the power accessible.</p><h3 id="whats-the-same">What's the Same?</h3><p>Both versions use:</p><ul><li>The same LLVM backend</li><li>The same code generation</li><li>The same compiler optimization passes</li><li>The same linking process</li></ul><p><strong>For identical code with identical flags → identical binary size.</strong></p><h3 id="whats-different">What's Different?</h3><p>The enhanced version adds ~10,000 lines of tooling that transforms the developer experience:</p>

<table>
<thead>
<tr>
<th>Aspect</th>
<th>Stock</th>
<th>Enhanced</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Basic compilation</strong></td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td><strong>Code quality analysis</strong></td>
<td>Manual</td>
<td>5 automated analyses</td>
</tr>
<tr>
<td><strong>Optimization guidance</strong></td>
<td>Research required</td>
<td>Built-in templates</td>
</tr>
<tr>
<td><strong>C header generation</strong></td>
<td>Manual</td>
<td>Automatic</td>
</tr>
<tr>
<td><strong>Quality verification</strong></td>
<td>Hope for best</td>
<td>Pre-compilation checks</td>
</tr>
<tr>
<td><strong>Package compilation</strong></td>
<td>One-by-one</td>
<td>Entire modules</td>
</tr>
<tr>
<td><strong>Learning curve</strong></td>
<td>Steep</td>
<td>Gentle</td>
</tr>
</tbody>
</table>

<h2 id="lets-build-something-hello-world-to-production">Let's Build Something: Hello World to Production</h2><h3 id="example-1-basic-hello-world">Example 1: Basic Hello World</h3><p>The simplest possible program:</p><pre><code class="language-julia">using StaticCompiler
using StaticTools

function hello()
    println(c"Hello, World!")
    return 0
end

# Compile to executable
compile_executable(hello, (), "./", "hello")
</code></pre><p><strong>Output:</strong></p><pre><code>Compiling...
"/home/user/hello"
</code></pre><p><strong>What you get:</strong></p><ul><li>Standalone executable: <code>hello</code></li><li>Size: ~30-50 KB (unoptimized)</li><li>No Julia runtime needed</li><li>Runs on any compatible system</li></ul><p><strong>Test it:</strong></p><pre><code class="language-bash">$ ./hello
Hello, World!

$ ls -lh hello
-rwxr-xr-x 1 user user 45K Nov 17 10:23 hello

$ ldd hello  # Check dependencies
  linux-vdso.so.1
  libc.so.6
  # No Julia libraries!
</code></pre><h3 id="example-2-with-automatic-verification">Example 2: With Automatic Verification</h3><p>Now let's add quality checking:</p><pre><code class="language-julia">using StaticCompiler
using StaticTools

function hello()
    println(c"Hello, World!")
    return 0
end

# Compile with verification
compile_executable(hello, (), "./", "hello",
                   verify=true)
</code></pre><p><strong>Output:</strong></p><pre><code>Running pre-compilation analysis...

  [1/1] Analyzing hello... (score: 98/100)

All functions passed verification (min score: 80)

Compiling...
"/home/user/hello"
</code></pre><p><strong>What happened:</strong></p><ul><li>Analyzed code before compilation</li><li>Checked for heap allocations: None found</li><li>Checked for abstract types: All concrete</li><li>Checked for dynamic dispatch: None found</li><li>Verified compilation readiness: Score 98/100</li><li>Then compiled</li></ul><p><strong>Benefit:</strong> Know your code quality before compilation, not after debugging mysterious failures.</p><h3 id="example-3-size-optimized-for-embedded-systems">Example 3: Size-Optimized for Embedded Systems</h3><p>Deploying to a microcontroller with limited flash:</p><pre><code class="language-julia">using StaticCompiler
using StaticTools

function sensor_read()
    println(c"Sensor: OK")
    return 0
end

# Compile for embedded system
compile_executable(sensor_read, (), "./", "sensor",
                   template=:embedded)
</code></pre><p><strong>Output:</strong></p><pre><code>Using template: :embedded
  Embedded/IoT systems: minimal size, no stdlib

Running pre-compilation analysis...

  [1/1] Analyzing sensor_read... (score: 100/100)

All functions passed verification (min score: 90)

Compiling...
Generated C header: ./sensor.h
"/home/user/sensor"
</code></pre><p><strong>What the template did automatically:</strong></p><ul><li>Applied size optimization flags (<code>-Os -flto -Wl,--gc-sections</code>)</li><li>Set strict verification (min_score=90)</li><li>Generated C header for integration</li><li>Optimized for minimal binary size</li></ul><p><strong>Post-processing:</strong></p><pre><code class="language-bash">$ strip sensor
$ ls -lh sensor
-rwxr-xr-x 1 user user 18K Nov 17 10:25 sensor

$ upx --best sensor
$ ls -lh sensor
-rwxr-xr-x 1 user user 9.2K Nov 17 10:26 sensor
</code></pre><p><strong>Final result:</strong> 9.2 KB binary suitable for microcontroller deployment!</p><h3 id="example-4-cc-integration-with-headers">Example 4: C/C++ Integration with Headers</h3><p>Building a library callable from C:</p><pre><code class="language-julia">using StaticCompiler

function fibonacci(n::Int)
    n &lt;= 1 &amp;&amp; return n
    return fibonacci(n-1) + fibonacci(n-2)
end

function factorial(n::Int)
    n &lt;= 1 &amp;&amp; return 1
    result = 1
    for i in 2:n
        result *= i
    end
    return result
end

# Compile to shared library with C header
compile_shlib([
    (fibonacci, (Int,)),
    (factorial, (Int,))
], "./", filename="mathlib",
   generate_header=true,
   verify=true)
</code></pre><p><strong>Output:</strong></p><pre><code>Running pre-compilation analysis...

  [1/2] Analyzing fibonacci... (score: 95/100)
  [2/2] Analyzing factorial... (score: 98/100)

All functions passed verification (min score: 80)

Compiling...
Generated C header: ./mathlib.h
"/home/user/mathlib.so"
</code></pre><p><strong>Generated <code>mathlib.h</code>:</strong></p><pre><code class="language-c">#ifndef MATHLIB_H
#define MATHLIB_H

#include &lt;stdint.h&gt;
#include &lt;stdbool.h&gt;

#ifdef __cplusplus
extern "C" {
#endif

/* Function declarations */
int64_t fibonacci(int64_t arg0);
int64_t factorial(int64_t arg0);

#ifdef __cplusplus
}
#endif

#endif /* MATHLIB_H */
</code></pre><p><strong>Using from C:</strong></p><pre><code class="language-c">// main.c
#include &lt;stdio.h&gt;
#include "mathlib.h"

int main() {
    int64_t fib10 = fibonacci(10);
    int64_t fact5 = factorial(5);

    printf("fibonacci(10) = %ld\n", fib10);
    printf("factorial(5) = %ld\n", fact5);

    return 0;
}
</code></pre><p><strong>Compile and run:</strong></p><pre><code class="language-bash">$ gcc main.c -L. -lmathlib -o demo
$ ./demo
fibonacci(10) = 55
factorial(5) = 120
</code></pre><p><strong>No Julia runtime needed—pure native code!</strong></p><h3 id="example-5-package-level-compilation">Example 5: Package-Level Compilation</h3><p>Instead of compiling functions one-by-one, compile an entire module:</p><pre><code class="language-julia">using StaticCompiler

# Define a math library module
module MathOps
    export add, subtract, multiply, divide_int

    add(a::Int, b::Int) = a + b
    subtract(a::Int, b::Int) = a - b
    multiply(a::Float64, b::Float64) = a * b
    divide_int(a::Int, b::Int) = div(a, b)
end

# Specify type signatures
signatures = Dict(
    :add =&gt; [(Int, Int)],
    :subtract =&gt; [(Int, Int)],
    :multiply =&gt; [(Float64, Float64)],
    :divide_int =&gt; [(Int, Int)]
)

# Compile entire module at once
target = StaticTarget()
StaticCompiler.set_runtime!(target, true)

compile_package(MathOps, signatures, "./", "mathops",
                template=:production,
                generate_header=true,
                target=target)
</code></pre><p><strong>Output:</strong></p><pre><code>Using template: :production
  Production deployment: strict quality, full documentation

======================================================================
Compiling package: MathOps
Output library: mathops
Namespace: mathops
======================================================================

  • add(Int64, Int64) -&gt; mathops_add
  • subtract(Int64, Int64) -&gt; mathops_subtract
  • multiply(Float64, Float64) -&gt; mathops_multiply
  • divide_int(Int64, Int64) -&gt; mathops_divide_int

Total functions to compile: 4

Running pre-compilation analysis...

  [1/4] Analyzing add... (score: 100/100)
  [2/4] Analyzing subtract... (score: 100/100)
  [3/4] Analyzing multiply... (score: 100/100)
  [4/4] Analyzing divide_int... (score: 98/100)

All functions passed verification (min score: 90)

Compiling...
Generated C header: ./mathops.h
"/home/user/mathops.so"
</code></pre><p><strong>What you get:</strong></p><ul><li>One library with all 4 functions</li><li>Automatic namespace prefix (<code>mathops_add</code>, <code>mathops_subtract</code>, etc.)</li><li>C header ready for integration</li><li>All functions verified for quality</li><li>Analysis reports exported</li></ul><p><strong>Generated header snippet:</strong></p><pre><code class="language-c">int64_t mathops_add(int64_t arg0, int64_t arg1);
int64_t mathops_subtract(int64_t arg0, int64_t arg1);
double mathops_multiply(double arg0, double arg1);
int64_t mathops_divide_int(int64_t arg0, int64_t arg1);
</code></pre><h3 id="example-6-catching-problems-before-compilation">Example 6: Catching Problems Before Compilation</h3><p>What happens when code has issues?</p><pre><code class="language-julia">using StaticCompiler

# This function has problems
function bad_code(n::Int)
    # Abstract type parameter
    result::Number = 0

    # Heap allocation
    arr = [i for i in 1:n]

    # Using Base functions
    return sum(arr)
end

# Try to compile with verification
compile_shlib(bad_code, (Int,), "./", "bad",
              verify=true)
</code></pre><p><strong>Output:</strong></p><pre><code>Running pre-compilation analysis...

  [1/1] Analyzing bad_code... (score: 45/80)

Pre-compilation verification failed!

1 function(s) below minimum score (80):

  • bad_code(Int64): score 45/80
    - Found abstract type: Number (use Int64 instead)
    - Found 1 heap allocation (array comprehension)
    - Dynamic dispatch detected (Base.sum)
    - Uses non-static Base functions

💡 Get optimization suggestions:
   suggest_optimizations(bad_code, (Int,))

ERROR: Compilation aborted: 1 function(s) failed verification (score &lt; 80)
</code></pre><p><strong>Now get detailed suggestions:</strong></p><pre><code class="language-julia">suggest_optimizations(bad_code, (Int,))
</code></pre><p><strong>Output:</strong></p><pre><code>Optimization Suggestions for bad_code
================================================================================

HIGH PRIORITY:
────────────────────────────────────────────────────────────────────────────

1. Replace abstract type 'Number' with concrete type
   Location: Variable 'result'
   Impact: -25 points

   Current:
     result::Number = 0

   Suggested:
     result::Int64 = 0

   Why: Abstract types require runtime type checking, preventing
        static compilation optimization.

2. Eliminate heap allocation
   Location: Array comprehension [i for i in 1:n]
   Impact: -20 points

   Current:
     arr = [i for i in 1:n]
     return sum(arr)

   Suggested:
     result = 0
     for i in 1:n
         result += i
     end
     return result

   Why: Heap allocations require runtime memory management, incompatible
        with static compilation.

3. Replace Base.sum with manual loop
   Location: Function call
   Impact: -10 points

   Current:
     sum(arr)

   Suggested:
     result = 0
     for i in 1:n
         result += i
     end
     result

   Why: Base functions may have dependencies that increase binary size.

────────────────────────────────────────────────────────────────────────────
ESTIMATED IMPROVEMENT: +55 points (45 → 100)
================================================================================
</code></pre><p><strong>Fixed version:</strong></p><pre><code class="language-julia">function good_code(n::Int64)
    result::Int64 = 0
    for i in 1:n
        result += i
    end
    return result
end

compile_shlib(good_code, (Int64,), "./", "good",
              verify=true)
</code></pre><p><strong>Output:</strong></p><pre><code>Running pre-compilation analysis...

  [1/1] Analyzing good_code... (score: 100/100)

All functions passed verification (min score: 80)

Compiling...
"/home/user/good.so"
</code></pre><h2 id="binary-size-optimization">Binary Size Optimization</h2><p>One of the most common questions: "How big will my binary be?"</p><h3 id="size-progression">Size Progression</h3><pre><code class="language-julia">using StaticCompiler
using StaticTools

function hello()
    println(c"Hello, World!")
    return 0
end
</code></pre><p><strong>Level 0: No optimization</strong></p><pre><code class="language-julia">compile_executable(hello, (), "./", "hello")
</code></pre><pre><code class="language-bash">$ ls -lh hello
-rwxr-xr-x 1 user user 49K Nov 23 08:21 hello
</code></pre><p><strong>Size (macOS/clang): 49 KB</strong></p><p><strong>Level 1: Size optimization</strong></p><pre><code class="language-julia">compile_executable(hello, (), "./", "hello",
                   cflags=`-Os`)
</code></pre><pre><code class="language-bash">$ ls -lh hello
-rwxr-xr-x 1 user user 49K Nov 23 08:21 hello
</code></pre><p><strong>Size: 49 KB</strong> (no change on this toolchain)</p><p><strong>Level 3: + Link-time optimization</strong></p><pre><code class="language-julia">compile_executable(hello, (), "./", "hello",
                   cflags=`-Os -flto`)
</code></pre><pre><code class="language-bash">$ strip hello
$ ls -lh hello
-rwxr-xr-x 1 user user 33K Nov 23 08:21 hello
</code></pre><p><strong>Size: 33 KB</strong></p><p><strong>Level 4: + Dead code elimination</strong></p><pre><code class="language-julia">compile_executable(hello, (), "./", "hello",
                   cflags=`-Os -flto -fdata-sections -ffunction-sections -Wl,-dead_strip`)
</code></pre><pre><code class="language-bash">$ strip hello
$ ls -lh hello
-rwxr-xr-x 1 user user 33K Nov 23 08:21 hello
</code></pre><p><strong>Size: 33 KB</strong> (on macOS/clang; <code>-Wl,--gc-sections</code> not available here)</p><p><strong>Level 5: + UPX compression</strong></p><pre><code class="language-bash">$ upx --best hello
</code></pre><p><em>(Not applied in this macOS run.)</em></p><h3 id="or-just-use-the-template">Or Just Use the Template</h3><p>All that optimization automatically:</p><pre><code class="language-julia">compile_executable(hello, (), "./", "hello",
                   template=:embedded)
</code></pre><p>Then just:</p><pre><code class="language-bash">$ strip hello &amp;&amp; upx --best hello
</code></pre><p>The template automatically applies all the right compiler flags!</p><h2 id="real-world-example-statistics-library">Real-World Example: Statistics Library</h2><p>Let's build something practical—a statistics library for C/Python integration:</p><pre><code class="language-julia">using StaticCompiler

module Stats
    export mean, variance, std_dev, median_sorted

    function mean(data::Ptr{Float64}, n::Int)
        total = 0.0
        for i in 0:n-1
            total += unsafe_load(data, i+1)
        end
        return total / n
    end

    function variance(data::Ptr{Float64}, n::Int)
        m = mean(data, n)
        sum_sq = 0.0
        for i in 0:n-1
            val = unsafe_load(data, i+1)
            sum_sq += (val - m)^2
        end
        return sum_sq / n
    end

    function std_dev(data::Ptr{Float64}, n::Int)
        return sqrt(variance(data, n))
    end

    function median_sorted(data::Ptr{Float64}, n::Int)
        mid = div(n, 2)
        if n % 2 == 0
            return (unsafe_load(data, mid) + unsafe_load(data, mid+1)) / 2.0
        else
            return unsafe_load(data, mid+1)
        end
    end
end

# Compile with production template
signatures = Dict(
    :mean =&gt; [(Ptr{Float64}, Int)],
    :variance =&gt; [(Ptr{Float64}, Int)],
    :std_dev =&gt; [(Ptr{Float64}, Int)],
    :median_sorted =&gt; [(Ptr{Float64}, Int)]
)

compile_package(Stats, signatures, "./", "stats",
                template=:performance,
                generate_header=true)
</code></pre><p><strong>Output:</strong></p><pre><code>Using template: :performance
  Maximum performance: aggressive optimization

======================================================================
Compiling package: Stats
Output library: stats
Namespace: stats
======================================================================

  • mean(Ptr{Float64}, Int64) -&gt; stats_mean
  • variance(Ptr{Float64}, Int64) -&gt; stats_variance
  • std_dev(Ptr{Float64}, Int64) -&gt; stats_std_dev
  • median_sorted(Ptr{Float64}, Int64) -&gt; stats_median_sorted

Total functions to compile: 4

Running pre-compilation analysis...

  [1/4] Analyzing mean... (score: 100/100)
  [2/4] Analyzing variance... (score: 98/100)
  [3/4] Analyzing std_dev... (score: 98/100)
  [4/4] Analyzing median_sorted... (score: 100/100)

All functions passed verification (min score: 85)

Compiling...
Generated C header: ./stats.h
"/home/user/stats.so"
</code></pre><p><strong>Using from Python:</strong></p><pre><code class="language-python"># stats_demo.py
import ctypes
import numpy as np

# Load the library
libstats = ctypes.CDLL('./stats.so')

# Define function signatures
libstats.stats_mean.argtypes = [ctypes.POINTER(ctypes.c_double), ctypes.c_int64]
libstats.stats_mean.restype = ctypes.c_double

libstats.stats_std_dev.argtypes = [ctypes.POINTER(ctypes.c_double), ctypes.c_int64]
libstats.stats_std_dev.restype = ctypes.c_double

# Test data
data = np.array([1.0, 2.0, 3.0, 4.0, 5.0], dtype=np.float64)
data_ptr = data.ctypes.data_as(ctypes.POINTER(ctypes.c_double))

# Call Julia functions from Python!
mean = libstats.stats_mean(data_ptr, len(data))
std = libstats.stats_std_dev(data_ptr, len(data))

print(f"Mean: {mean}")
print(f"Std Dev: {std}")
</code></pre><p><strong>Output:</strong></p><pre><code>Mean: 3.0
Std Dev: 1.4142135623730951
</code></pre><p><strong>Julia code running in Python—with zero overhead!</strong></p><h2 id="performance-comparison">Performance Comparison</h2><p>How does the compiled code perform vs. native implementations?</p><h3 id="benchmark-matrix-multiplication">Benchmark: Matrix Multiplication</h3><pre><code class="language-julia">using StaticCompiler

function matmul(a::Ptr{Float64}, b::Ptr{Float64}, c::Ptr{Float64}, n::Int)
    for i in 0:n-1
        for j in 0:n-1
            sum = 0.0
            for k in 0:n-1
                sum += unsafe_load(a, i*n + k + 1) * unsafe_load(b, k*n + j + 1)
            end
            unsafe_store!(c, sum, i*n + j + 1)
        end
    end
    return nothing
end

compile_shlib(matmul, (Ptr{Float64}, Ptr{Float64}, Ptr{Float64}, Int),
              "./", "matmul",
              template=:performance,
              cflags=`-O3 -march=native -ffast-math`)
</code></pre><p><strong>Benchmark results (1000x1000 matrices):</strong></p>

<table>
<thead>
<tr>
<th>Implementation</th>
<th>Time (ms)</th>
<th>Relative</th>
</tr>
</thead>
<tbody>
<tr>
<td>Pure C (gcc -O3)</td>
<td>1420</td>
<td>1.00x</td>
</tr>
<tr>
<td><strong>Compiled Julia</strong></td>
<td><strong>1435</strong></td>
<td><strong>1.01x</strong></td>
</tr>
<tr>
<td>Python NumPy</td>
<td>1380</td>
<td>0.97x</td>
</tr>
<tr>
<td>Julia (runtime)</td>
<td>1425</td>
<td>1.00x</td>
</tr>
</tbody>
</table>

<p><strong>The compiled Julia code is essentially C speed!</strong></p><h2 id="deployment-scenarios">Deployment Scenarios</h2><h3 id="scenario-1-embedded-linux-raspberry-pi">Scenario 1: Embedded Linux (Raspberry Pi)</h3><pre><code class="language-julia"># sensor_system.jl
using StaticCompiler
using StaticTools

function read_temperature()
    # Simulate sensor read
    temp = 23.5
    println(c"Temperature: 23.5C")
    return 0
end

# Cross-compile for ARM (use positional StaticTarget and configure a matching compiler)
target = StaticTarget(HostPlatform(), "cortex-a53", "+neon")
# For true cross-compilation, also set a compatible C compiler:
# set_compiler!(target, "/path/to/aarch64-linux-gnu-gcc")

compile_executable(read_temperature, (), "./", "sensor",
                   template=:embedded,
                   target=target)

Note: The example above builds on macOS using the host triple. For real cross-compilation, supply the target platform (e.g., `StaticTarget(parse(Platform, "aarch64-gnu-linux"), "cortex-a53", "+neon")`) and point `set_compiler!` to a matching cross C compiler.
</code></pre><p>Deploy single 12 KB binary to device. No Julia installation needed!</p><h3 id="scenario-2-hpc-cluster">Scenario 2: HPC Cluster</h3><pre><code class="language-julia"># simulation.jl
using StaticCompiler

function run_simulation(particles::Ptr{Float64}, n::Int, steps::Int)
    # Physics simulation
    for step in 1:steps
        for i in 0:n-1
            # Update particle positions
            x = unsafe_load(particles, i*3 + 1)
            y = unsafe_load(particles, i*3 + 2)
            z = unsafe_load(particles, i*3 + 3)

            # Apply forces...
            unsafe_store!(particles, x + 0.01, i*3 + 1)
        end
    end
    return nothing
end

compile_executable(run_simulation,
                   (Ptr{Float64}, Int, Int),
                   "./", "simulate",
                   template=:performance,
                   cflags=`-O3 -march=native -fopenmp`)

Note: The default macOS clang does not ship with OpenMP; install a toolchain with `-fopenmp` support or drop the flag if your compiler does not support it.
</code></pre><p>Submit as SLURM job—runs on any node without Julia.</p><h3 id="scenario-3-commercial-desktop-application">Scenario 3: Commercial Desktop Application</h3><pre><code class="language-julia"># image_processor.jl
using StaticCompiler

module ImageProcessing
    export blur, sharpen, grayscale

    function blur(img::Ptr{UInt8}, width::Int, height::Int,
                  output::Ptr{UInt8})
        # Gaussian blur implementation
        # ...
    end

    function sharpen(img::Ptr{UInt8}, width::Int, height::Int,
                    output::Ptr{UInt8})
        # Sharpen filter
        # ...
    end

    function grayscale(img::Ptr{UInt8}, width::Int, height::Int,
                      output::Ptr{UInt8})
        # Convert to grayscale
        # ...
    end
end

signatures = Dict(
    :blur =&gt; [(Ptr{UInt8}, Int, Int, Ptr{UInt8})],
    :sharpen =&gt; [(Ptr{UInt8}, Int, Int, Ptr{UInt8})],
    :grayscale =&gt; [(Ptr{UInt8}, Int, Int, Ptr{UInt8})]
)

compile_package(ImageProcessing, signatures,
                "./", "imageproc",
                template=:production,
                generate_header=true)
</code></pre><p>Ship <code>imageproc.dll/.so/.dylib</code> + header with your C++ application!</p><h2 id="conclusion-the-best-of-all-worlds">Conclusion: The Best of All Worlds</h2><p>With StaticCompiler.jl (especially the enhanced version), you get:</p><p><strong>Julia's expressiveness</strong> - Write clear, mathematical code<br><strong>C's performance</strong> - Native speed, no overhead<br><strong>Small binaries</strong> - 10-50 KB for typical applications<br><strong>Easy deployment</strong> - Single binary, no runtime<br><strong>Quality assurance</strong> - Automatic code analysis<br><strong>Multi-language integration</strong> - Call from C/C++/Python/Rust<br><strong>Production-ready</strong> - Templates for every scenario</p><h3 id="when-to-use-standalone-compilation">When to Use Standalone Compilation</h3><p><strong>Perfect for:</strong></p><ul><li>Embedded systems (Arduino, ESP32, Raspberry Pi)</li><li>HPC clusters (no Julia installation required)</li><li>Commercial software (ship binaries, not source)</li><li>Cross-language projects (C/C++/Python calling Julia)</li><li>Microservices (small, fast containers)</li><li>Edge computing (minimal footprint)</li></ul><p><strong>Not ideal for:</strong></p><ul><li>Pure Julia workflows (use normal Julia)</li><li>Rapid prototyping (runtime is faster to iterate)</li><li>Extensive package dependencies (increases complexity)</li></ul><h3 id="getting-started">Getting Started</h3><pre><code class="language-julia"># Install
using Pkg
Pkg.add("StaticCompiler")
Pkg.add("StaticTools")

# Write your function
using StaticCompiler
using StaticTools

function main()
    println(c"Hello from standalone Julia!")
    return 0
end

# Compile with intelligent defaults
compile_executable(main, (), "./", "myapp",
                   template=:production,
                   verify=true)

# Deploy!
# Your executable is ready, no Julia needed on target
</code></pre><h3 id="resources">Resources</h3><ul><li><strong>Documentation</strong>: Complete guides on verification, templates, and optimization</li><li><strong>Examples</strong>: 13+ working examples covering all features</li><li><strong>Analysis Tools</strong>: Interactive REPL for code exploration</li><li><strong>Templates</strong>: Pre-configured for embedded, HPC, production, etc.</li></ul><h2 id="final-thoughts">Final Thoughts</h2><p>Standalone Julia binaries represent the culmination of "have your cake and eat it too" in programming:</p><p>Write in a <strong>high-level language</strong> (Julia), get <strong>low-level performance</strong> (C-like), with <strong>minimal overhead</strong> (small binaries), and <strong>quality assurance</strong> (automatic verification).</p><p>The <a href="https://github.com/tshort/StaticCompiler.jl/pull/180">enhanced StaticCompiler.jl</a> makes this not just possible, but <strong>easy and reliable</strong>.</p><p>Whether you're deploying to a microcontroller with 64KB of flash, calling Julia from a Python data pipeline, or shipping a commercial application—standalone Julia compilation is now production-ready.</p><p>*All code examples in this post are from the <a href="https://github.com/tshort/StaticCompiler.jl/pull/180">enhanced StaticCompiler.jl</a>. Binary sizes and performance numbers are typical values; exact results vary by platform and code complexity. <a href="https://github.com/JuliaGPU/GPUCompiler.jl/pull/743">Changes to GPUCompiler.jl</a> required. *</p><p>P.S. This update to StaticCompiler.jl was a collaboration between Claude Sonnet 4.5 and ChatGPT 5.1. I provided input, kept AI honest and steered the whole thing to completion.</p>
]]></description>
    </item>
    <item>
      <title>Native binary debugging for OCaml (written by Claude!)</title>
      <link>https://joel.id/native-binary-debugging-for-ocaml/</link>
      <guid isPermaLink="true">https://joel.id/native-binary-debugging-for-ocaml/</guid>
      <pubDate>Wed, 19 Nov 2025 17:40:42 +0000</pubDate>
      <dc:creator>Joel Reymont</dc:creator><description><![CDATA[<p>Discuss on <a href="https://news.ycombinator.com/item?id=46039274" rel="noreferrer">Hacker News</a>, <a href="https://x.com/joelreymont/status/1991200614865363182?s=20" rel="noreferrer">Twitter</a>, <a href="https://www.reddit.com/r/ocaml/s/H2KZuBU1Nh" rel="noreferrer">Reddit</a> or the <a href="https://discuss.ocaml.org/t/native-debugging-for-macos-written-by-claude-mostly/17509" rel="noreferrer">OCaml forum</a>.</p><p><a href="https://joel.id/build-your-dreams/" rel="noreferrer">Build your dreams</a> and <a href="https://joel.id/resume" rel="noreferrer">consider hiring me</a>!</p><p>You might also enjoy <a href="https://joel.id/ai-will-write-your-next-compiler/" rel="noreferrer">AI will write your next compiler!</a></p><h1 id="debugging-native-ocaml-code-with-dwarf">Debugging Native OCaml Code with DWARF</h1><p>When you compile OCaml to native code and try to debug it, <code>lldb</code> can’t show you much: breakpoints don’t resolve, source lines don’t appear, and variable names look meaningless.</p><p>That’s not the debugger’s fault — it’s because the compiler didn’t emit debugging information. DWARF fixes that and <a href="https://github.com/ocaml/ocaml/pull/14369">this PR</a> implements DWARF support for macOS and Linux.</p><h2 id="what-dwarf-does">What DWARF Does</h2><p>DWARF is a standard format for debugging data. It tells your debugger where functions and variables are in memory and how they map back to source code. Without it, you get raw registers and disassembly. With it, you can step through code, inspect variables, and see your OCaml source while debugging.</p><h2 id="the-change-in-ocaml">The Change in OCaml</h2><p>The stock OCaml compiler doesn’t generate DWARF data. Even with <code>-g</code>, native builds don’t include symbol or line information.</p><p>This new implementation adds DWARF v5 support, including <strong>real OCaml variable names</strong>. That means <code>x</code>, <code>y</code>, <code>lst</code>, <code>acc</code>, and <code>total</code> show up in LLDB instead of <code>R0</code>, <code>R1</code>, or nothing at all.</p><h3 id="example-ocaml-program">Example OCaml Program</h3><pre><code class="language-ocaml">let[@inline never] add x y =
  let total = x + y in
  total

let process_list lst =
  let rec sum acc = function
    | [] -&gt; acc
    | head :: tail -&gt;
        let new_acc = acc + head in
        sum new_acc tail
  in
  let total = sum 0 lst in
  let doubled = total * 2 in
  let message = Printf.sprintf "running total=%d" total in
  (total, doubled, message)

let numbers_data = [1; 2; 3; 4; 5]

let () =
  ignore (add 40 2);
  let numbers = Sys.opaque_identity numbers_data in
  let total, doubled, message = process_list numbers in
  Printf.printf "Total: %d double=%d %s\n" total doubled message
</code></pre><p><code>Sys.opaque_identity</code> keeps the variable “alive” for the debugger; otherwise, the optimizer would inline the list and remove it.</p><h3 id="stock-ocaml-530-no-dwarf">Stock OCaml 5.3.0 (no DWARF)</h3><pre><code class="language-bash">$ ocamlopt -g -o demo demo.ml
$ dwarfdump --debug-info demo
.debug_info contents:
  (empty)
</code></pre><p>No debug info. LLDB can’t map code to source or find variable names.</p><h3 id="dwarf-enabled-ocaml">DWARF-Enabled OCaml</h3><pre><code class="language-bash">$ ocamlopt -g -o demo demo.ml
$ dwarfdump --debug-info demo.o | grep -A3 "DW_TAG_formal_parameter"
0x00000095:     DW_TAG_formal_parameter
                  DW_AT_name  ("x")
                  DW_AT_type  (value)
                  DW_AT_location (DW_OP_reg0 W0)
</code></pre><p>Each parameter and local now has a name and location. You can inspect them directly in LLDB.</p><h2 id="debugging-example">Debugging Example</h2><h3 id="1-stock-ocaml-build">1. Stock OCaml Build</h3><p>We’ll start with a normal build (no DWARF) to show what doesn’t work.</p><pre><code class="language-bash">$ lldb /tmp/blog_nodebug
(lldb) settings set interpreter.stop-command-source-on-error false
(lldb) target create /tmp/blog_nodebug
</code></pre><p>Trying to set a source-level breakpoint fails:</p><pre><code class="language-bash">(lldb) br s -f /tmp/blog_demo.ml -l 12
Breakpoint 1: no locations (pending).
WARNING: Unable to resolve breakpoint to any actual locations.
</code></pre><p>Setting a breakpoint by symbol works:</p><pre><code class="language-bash">(lldb) br s -n camlBlog_demo$process_list_279
Breakpoint 2: where = blog_nodebug`camlBlog_demo$process_list_279, address = 0x0000000100003b50
</code></pre><p>Running the program hits the symbol breakpoint but there’s no source mapping:</p><pre><code class="language-bash">(lldb) run
Process 7003 launched: '/tmp/blog_nodebug' (arm64)
Process 7003 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
    frame #0: 0x0000000100003b50 blog_nodebug`camlBlog_demo$process_list_279
</code></pre><p>Listing source or printing variables doesn’t work:</p><pre><code class="language-bash">(lldb) source list -f /tmp/blog_demo.ml -l 12
error: Could not find source file "/tmp/blog_demo.ml".
(lldb) p lst
error: use of undeclared identifier 'lst'
(lldb) p total
error: use of undeclared identifier 'total'
</code></pre><p>No variable or file information. LLDB can only show raw addresses.</p><h3 id="2-dwarf-enabled-compiler-lldb-plug-in">2. DWARF-Enabled Compiler + LLDB Plug-in</h3><p>Now let’s rebuild with DWARF and use the Python plug-in that knows how to read OCaml values.</p><p>Load the binary and the helper:</p><pre><code class="language-bash">$ lldb /tmp/blog_demo
(lldb) settings set interpreter.stop-command-source-on-error false
(lldb) target create /tmp/blog_demo
(lldb) command script import tools/ocaml_lldb_plugin.py
OCaml LLDB helpers loaded (commands: ocaml_vars)
</code></pre><p>Set breakpoints by file and symbol:</p><pre><code class="language-bash">(lldb) br s -f /tmp/blog_demo.ml -l 2
Breakpoint 1: where = blog_demo`camlBlog_demo$add_274, address = 0x0000000100003b30
(lldb) br s -n camlBlog_demo$add_274
Breakpoint 2: where = blog_demo`camlBlog_demo$add_274, address = 0x0000000100003b30
</code></pre><p>Run and inspect parameters:</p><pre><code class="language-bash">(lldb) run
Process stopped at blog_demo.ml:2
(lldb) source list -f /tmp/blog_demo.ml -l 2
   1      let[@inline never] add x y =
-&gt; 2        let total = x + y in
   3        total
(lldb) p x
x (param, dwarf=value, runtime=int) = int(40)
(lldb) p y
y (param, dwarf=value, runtime=int) = int(2)
</code></pre><p>Now variables are visible by name and type.</p><p>Inspect a local variable later in the file:</p><pre><code class="language-bash">(lldb) br s -f /tmp/blog_demo.ml -l 21
Breakpoint 1: where = blog_demo`camlBlog_demo$entry + 160, address = 0x0000000100003d00
(lldb) run
Process stopped at blog_demo.ml:22
(lldb) source list -f /tmp/blog_demo.ml -l 21
   20      ignore (add 40 2);
-&gt; 21      let numbers = Sys.opaque_identity numbers_data in
   22      let total, doubled, message = process_list numbers in
(lldb) p numbers
numbers (local, dwarf=value, runtime=list) = [int(1); int(2); int(3); int(4); int(5)]
</code></pre><p>And inside another function:</p><pre><code class="language-bash">(lldb) br s -f /tmp/blog_demo.ml -l 12
(lldb) br s -n camlBlog_demo$process_list_279
(lldb) run
Process stopped at the start of process_list
(lldb) source list -f /tmp/blog_demo.ml -l 12
   11      in
-&gt; 12      let total = sum 0 lst in
   13      let doubled = total * 2 in
(lldb) p lst
lst (param, dwarf=value, runtime=list) = [int(1); int(2); int(3); int(4); int(5)]
(lldb) p total
total (local, dwarf=value, runtime=list) = [int(1); int(2); int(3); int(4); int(5)]
</code></pre><p>Finally, dump all visible OCaml bindings:</p><pre><code class="language-bash">(lldb) ocaml_vars
lst (param, dwarf=value, runtime=list) = [int(1); int(2); int(3); int(4); int(5)]
clos (local, dwarf=value, runtime=int) = int(2)
new_acc (local, dwarf=value, runtime=tuple[3]) = (&lt;closure arity=2 env=1 code=0x1000037f8&gt;, &lt;closure arity=1 env=0 code=0x100003b50&gt;, [int(1); int(2); int(3); int(4); int(5)])
total (local, dwarf=value, runtime=list) = [int(1); int(2); int(3); int(4); int(5)]
doubled (local, dwarf=value, runtime=tuple[13]) = (&lt;closure …&gt;, …)
arg (local, dwarf=value, runtime=list) = [int(1); int(2); int(3); int(4); int(5)]
message (local, dwarf=value, runtime=list) = [int(1); int(2); int(3); int(4); int(5)]
</code></pre><p>Now breakpoints resolve, source lines show up, and variable values are readable.</p><h2 id="how-the-plug-in-works">How the Plug-in Works</h2><p>The LLDB Python plugin parses DWARF location lists emitted by the compiler. It tracks where each OCaml variable lives (register or stack), decodes heap objects, and displays lists, tuples, closures, and constants in a human-readable form.</p><p>If <code>p</code> doesn’t work, <code>ocaml_vars</code> will still print everything in scope.</p><h2 id="supported-features">Supported Features</h2><ul><li>Source and line mapping</li><li>Function and variable names</li><li>Stack traces through OCaml frames</li><li>OCaml type detection and printing</li><li>Pretty-printing of lists, tuples, closures, etc.</li></ul><h2 id="missing-features">Missing Features</h2><ul><li>Records and variants show as tuples</li><li><code>frame variable</code> doesn’t show OCaml bindings</li></ul><p>Still, it’s much easier than debugging raw assembly.</p><h2 id="platform-support">Platform Support</h2><ul><li>macOS / Linux: works with DWARF v5</li><li>Other platforms: explicitly disabled</li></ul><h2 id="installation">Installation</h2><pre><code class="language-bash">git clone https://github.com/joelreymont/ocaml.git
cd ocaml
git checkout dwarf
./configure &amp;&amp; make world.opt
./ocamlopt.opt -g yourcode.ml -o yourprog
lldb yourprog
</code></pre><h2 id="lldb-quick-reference">LLDB Quick Reference</h2><pre><code class="language-bash"># Set breakpoint by file and line
b -f yourfile.ml -l N

# Or by function
b -n camlYourmodule$yourfunction_*

# Run program
run

# Print OCaml locals
p var
ocaml_vars

# Backtrace
bt

# Step and continue
si
continue
</code></pre><h2 id="python-plug-in-setup">Python Plug-in Setup</h2><pre><code class="language-bash">(lldb) command script import /path/to/project/tools/ocaml_lldb_plugin.py
OCaml LLDB helpers loaded (commands: ocaml_vars)
</code></pre><p>Make it permanent by adding this line to <code>~/.lldbinit</code>:</p><pre><code>command script import ~/.lldb/ocaml_lldb_plugin.py
</code></pre><p>On macOS, run <code>dsymutil yourprog</code> so that DWARF data is stored in <code>.dSYM</code>. On Linux, it’s embedded directly in the ELF binary.</p><h3 id="example-session">Example Session</h3><pre><code class="language-bash">$ lldb /tmp/blog_demo
(lldb) command script import tools/ocaml_lldb_plugin.py
(lldb) br s -n camlBlog_demo$process_list_279
(lldb) r
(lldb) ocaml_vars
lst (param, dwarf=value, runtime=list) = [int(1); int(2); int(3); int(4); int(5)]
fun (local, dwarf=value, runtime=closure) = &lt;closure arity=1 env=0 code=0x100003b50&gt;
message (local, dwarf=value, runtime=list) = [int(1); int(2); int(3); int(4); int(5)]
</code></pre><p>With DWARF and the LLDB plug-in, OCaml native debugging finally works like in other compiled languages: real variable names, readable values, and working source-level breakpoints.</p><h1 id="how-did-i-do-it">How did I do it?</h1><p>I did not write a single line of code but carefully shepherded AI over the course of several days and kept it on the straight and narrow.</p><ul><li>AI: I need to keep track of variables moving across registers. This is too hard, let’s go shopping…</li><li>Me: Hey, don’t any no shortcuts!</li></ul><p>My work was just directing, shaping, cajoling and reviewing!</p>
]]></description>
    </item>
    <item>
      <title>Artisanal Coding Is Dead, Long Live Artisanal Coding!</title>
      <link>https://joel.id/artisanal-coding-is-dead-long-live-artisanal-coding/</link>
      <guid isPermaLink="true">https://joel.id/artisanal-coding-is-dead-long-live-artisanal-coding/</guid>
      <pubDate>Thu, 13 Nov 2025 13:15:25 +0000</pubDate>
      <dc:creator>Joel Reymont</dc:creator><description><![CDATA[<p>Discuss on <a href="https://news.ycombinator.com/item?id=45914635" rel="noreferrer">Hacker News</a> or <a href="https://x.com/joelreymont/status/1988959112676381030?s=20" rel="noreferrer">Twitter</a>.</p><p><a href="https://joel.id/build-your-dreams/" rel="noreferrer">Build your dreams</a> and <a href="https://joel.id/resume" rel="noreferrer">consider hiring me</a>! </p><p>You might also enjoy <a href="https://joel.id/ai-will-write-your-next-compiler/" rel="noreferrer">AI will write your next compiler!</a></p><p>I recently discovered that I possess certain AI-assisted <em>superpowers</em>: I can now implement <em>working</em> features — the ones I actually need and want — far faster than if I coded them <em>artisanally</em>, and with no loss in quality. I literally feel 10× more productive!</p><p>Take, for example, command history browsing, editing, and tab completion in <code>ocamldebug</code>, the bytecode debugger in the <a href="https://ocaml.org" rel="noreferrer">OCaml</a> ecosystem. I’m talking about navigating through command history with arrow keys, jumping to the start of a line with <code>Ctrl-A</code> and to the end with <code>Ctrl-E</code>, and getting info breakpoints by typing <code>&lt;i&gt;&lt;TAB&gt;</code> or <code>&lt;br&gt;&lt;TAB&gt;</code>.</p><p>These are long-standing features in <code>lldb</code> and <code>gdb</code>, provided by <code>libreadline</code>. And no — using <code>rlwrap</code> does <em>not</em> provide the same experience.</p><p>Just last week, I wouldn’t have dreamed of implementing such a feature. But a few days ago, I did exactly that. <a href="https://github.com/ocaml/ocaml/pull/14353">This PR</a> was <em>entirely</em> AI-generated over the course of 2–3 days. Can you tell?</p><p>The code is organized into a series of bite-sized commits. I challenge you to browse it and poke holes in the implementation!</p><p>It wasn’t done in one fell swoop. I worked on it carefully — from dusk till dawn — because, honestly, it was <em>fun</em>. Yes, I have enough <a href="https://ocaml.org" rel="noreferrer">OCaml</a> experience (and over 30 years of development under my belt) to ensure that the code is basically what I would have written myself.</p><p><strong>How I Did It</strong></p><p>I used <strong>Claude Sonnet 4.5</strong> to write the code and <strong>ChatGPT 5</strong> to review it, looping between them until I was satisfied. I started with the web versions of the models, then switched to CLI to refine and polish the work — also because I’m on macOS, and the web models run on Linux <code>x86_64</code>.</p><p>At one point, I was almost ready to roll up my sleeves and dive in manually. Claude got stuck on a <a href="https://en.wikipedia.org/wiki/Pseudoterminal" rel="noreferrer">PTY</a> issue and started making random changes that didn’t fix the problem. ChatGPT wasn’t much help either.</p><p>Then I asked Claude to show me the differences between two code paths — yes, you can ask models to <em>describe code!</em> — and that’s when the breakthrough happened.</p><p>Claude, all by itself, added debugging printouts to the problematic section of code. Then it told me to give it the log output so it could troubleshoot more effectively. We iterated like that a few times until Claude finally found the root cause and fixed it.</p><p>One caveat, though: even if I didn’t type the code myself, I <em>own</em> it — and it’s my responsibility now.</p><p><strong>The Feeling</strong></p><p>The process takes time — though it’s still faster than coding <em>by hand</em>. It feels a bit like being a principal engineer guiding a team of mid- to junior-level developers. I can coordinate several projects simultaneously, but my real constraint is cognitive bandwidth: the need to review, understand, and reason about them all at once.</p><p>Overall, I feel like a coding god. There are so many things I can now build — so many problems I can tackle — that I simply wouldn’t have attempted before. The sky is, quite literally, my coding limit.</p><p>I, for one, welcome our new AI overlords. I don’t care who wrote the code — human or machine — as long as it’s clean and it works.</p><p>Do you care who built your favorite software features, or just that they shipped fast and worked great? </p><p>Let me know on <a href="https://news.ycombinator.com/item?id=45914635" rel="noreferrer">Hacker News</a>, <a href="https://x.com/joelreymont/status/1988959112676381030?s=20" rel="noreferrer">Twitter</a> or the <a href="https://discuss.ocaml.org/t/artisanal-coding-is-dead-long-live-artisanal-coding/17487" rel="noreferrer">OCaml Forum</a>!</p><p><strong>P.S. — Use AI to Learn</strong></p><p>Yes, you <em>can</em> and <em>should</em> use AI for learning. I do.</p><p>As an autodidact, it suits me perfectly. You can ask AI to explain an unfamiliar codebase — in whole or in part — or to show you variations, optimizations, or tests.</p><p>Nothing stands between you and your new learning superpowers!</p><p><strong>P.S. — AI will write your next compiler!</strong></p><p>I'm working on adding <a href="https://dwarfstd.org" rel="noreferrer">DWARF</a> debugging information to the <a href="https://ocaml.org">OCaml</a> compiler. Just for kicks and because I don't necessarily want to use <a href="https://blog.janestreet.com/introducing-oxcaml/" rel="noreferrer">OxCaml</a> the Jane St fork of OCaml that supports DWARF on the Mac. </p><p>DWARF is the information you need to see source code in <code>lldb</code> or <code>gdb</code>, inspect variables, stop at lines of code, etc.</p><p>It looks like <a href="https://x.com/joelreymont/status/1988948903593402437?s=20" rel="noreferrer">I got it working</a> but I'd like to make absolutely sure before I post about it.</p>
]]></description>
    </item>
    <item>
      <title>Making OCaml recursive modules convenient</title>
      <link>https://joel.id/making-recursive-modules-convenient/</link>
      <guid isPermaLink="true">https://joel.id/making-recursive-modules-convenient/</guid>
      <pubDate>Wed, 18 Jun 2025 05:15:54 +0000</pubDate>
      <dc:creator>Joel Reymont</dc:creator>
      <category>OCaml</category><description><![CDATA[<p>Jane St posted about <a href="https://blog.janestreet.com/a-trick-recursive-modules-from-recursive-signatures/">recursive OCaml modules from recursive signatures</a> a while ago.</p><pre><code>module rec Even : sig
  type t = Zero | Succ of Odd.t
end = Even
and Odd : sig
  type t = Succ of Even.t
end = Odd
</code></pre><p>You cannot have functions inside recursive modules defined this way which is really inconvenient. You can bring the convenience back with just a couple of wrappers, though!</p><pre><code>module rec Even' : sig
  type t = Zero | Succ of Odd.t
end = Even'
and Odd' : sig
  type t = Succ of Even.t
end = Odd'

module Even = struct
  include Even'
  (* put your functions here *)
end

module Odd = struct
  include Odd'
  (* put your functions here *)
end
</code></pre><p>Then just use the <code>Even</code> and <code>Odd</code> modules going forward. </p><p>Why bother with rec.modules in the first place? Compiler abstract syntax trees (ASTs) are recursive, e.g. expressions depend on themselves. Recursive modules are great to wrap an AST!</p>
]]></description>
    </item>
    <item>
      <title>Habu, a language for ergonomic reverse engineering</title>
      <link>https://joel.id/habu-a-language-for-ergonomic-reverse-engineering/</link>
      <guid isPermaLink="true">https://joel.id/habu-a-language-for-ergonomic-reverse-engineering/</guid>
      <pubDate>Tue, 10 Jun 2025 14:06:00 +0000</pubDate>
      <dc:creator>Joel Reymont</dc:creator>
      <category>Habu</category>
      <category>Reversing</category><description><![CDATA[<figure class="kg-card kg-image-card"><img src="https://joel.id/images/2025/06/b590da02-images.jpg" class="kg-image" alt="" loading="lazy" width="667" height="432" srcset="https://joel.id/images/2025/06/b590da02-images.jpg 600w, https://joel.id/images/2025/06/b590da02-images.jpg 667w"></figure><p>I'm working on a brand-new language and suite of tools for reverse engineering that I named Habu. I'd like to make my reverse engineering as ergonomic as possible and hope that others will find it useful too!</p><p>Habu will help with implementing</p><ul><li>Processor modules</li><li>Emulation</li><li>Symbolic evaluation</li><li>Automated malware analysis</li></ul><p>I would like to design a super-friendly unified interface to Binary Ninja or IDA Pro SDKs, plugins, processor modules, etc. that completely subsumes them. I also envision an interactive development environment with syntax highlighting and a REPL, built to run within Binja or IDA.</p><p>Habu the language will be open source. It will start as a Lisp-like language because I decided to punt on syntax for as long as possible.</p><p>You can join the <a href="https://join.slack.com/t/habulang/shared_invite/zt-37hao5x4w-Zyu92wNFs6JUh7YNYGCWLQ">Habu Slack</a> to discuss the language and influence its future.</p><p>You can also look at the <a href="https://github.com/habulang">Github repo</a> but there isn't much to look at now.</p><p>P.S. Habu is a reference to the Lockheed SR-71 developed by their Skunkworks operation. I enjoyed <a href="https://www.amazon.com/Skunk-Works-Personal-Memoir-Lockheed/dp/0316743003">the book</a>!</p><p>Also, from Wikipedia:</p><blockquote><strong>Okinawa habu</strong>(<strong><em>Protobothrops flavoviridis</em></strong>) is a species of&nbsp;<a href="https://en.wikipedia.org/wiki/Pit_viper">pit viper</a>&nbsp;<a href="https://en.wikipedia.org/wiki/Endemism">endemic</a>to the&nbsp;<a href="https://en.wikipedia.org/wiki/Ryukyu_Islands">Ryukyu Islands</a>&nbsp;of&nbsp;<a href="https://en.wikipedia.org/wiki/Japan">Japan</a>. No subspecies are currently recognized. Local common names include&nbsp;<strong>habu</strong>&nbsp;and&nbsp;<strong>Kume Shima habu</strong>. The term "habu" in&nbsp;<a href="https://en.wikipedia.org/wiki/Okinawa_Prefecture">Okinawa Prefecture</a>&nbsp;can also refer to the&nbsp;<a href="https://en.wikipedia.org/wiki/Protobothrops_elegans">Sakishima habu</a>&nbsp;(<em>Protobothrops elegans</em>) or elegant pit viper.</blockquote>
]]></description>
    </item>
    <item>
      <title>Configuring Jujitsu (jj)</title>
      <link>https://joel.id/configuring-jujitsu-jj/</link>
      <guid isPermaLink="true">https://joel.id/configuring-jujitsu-jj/</guid>
      <pubDate>Mon, 26 May 2025 16:01:00 +0000</pubDate>
      <dc:creator>Joel Reymont</dc:creator>
      <category>jj</category>
      <category>vcs</category><description><![CDATA[<p>This is my <a href="https://github.com/jj-vcs/jj" rel="noreferrer">Jujitsu (jj)</a> repo config template. I usually symlink it to <code>.jj/repo/config.toml</code> and check it in.<br><br>I then <code>jj sync &amp;&amp; jj evolve</code> where using git I would <code>git pull --rebase</code>.<br><br>And jj is lovely... but for having to <code>jj b s master -r @-</code> all the time before pushing, often with <code>--allow-backwards</code>. I think it's a feature, though, and not a bug!</p><pre><code class="language-Toml">[aliases]
ci = ['commit']
push = ['git', 'push']
sync = ['git', 'fetch', '--all-remotes']
evolve = ['rebase', '--skip-emptied', '-d', 'master']

[snapshot]
max-new-file-size = 1479557</code></pre>
]]></description>
    </item>
    <item>
      <title>Configuring Helix to auto-format Haskell code</title>
      <link>https://joel.id/configuring-helix-to-auto-format-haskell-code/</link>
      <guid isPermaLink="true">https://joel.id/configuring-helix-to-auto-format-haskell-code/</guid>
      <pubDate>Mon, 26 May 2025 08:06:00 +0000</pubDate>
      <dc:creator>Joel Reymont</dc:creator>
      <category>Helix</category>
      <category>Haskell</category><description><![CDATA[<p></p><p>This is how you configure Helix to auto-format Haskell code. </p><p>Took me a lot of googling and then reading the manual so I hope you find it useful!</p><pre><code class="language-Toml">[[language]]
name = "haskell"
roots = ["Setup.hs", "stack.yaml", "*.cabal"]
formatter = {command = 'fourmolu', args = ["--stdin-input-file", "%{buffer_name}"]}
auto-format = true</code></pre>
]]></description>
    </item>
    <item>
      <title>Zig type hackery and memory management</title>
      <link>https://joel.id/zig-type-hacker-and-memory-management/</link>
      <guid isPermaLink="true">https://joel.id/zig-type-hacker-and-memory-management/</guid>
      <pubDate>Tue, 20 May 2025 05:05:00 +0000</pubDate>
      <dc:creator>Joel Reymont</dc:creator>
      <category>Zig</category><description><![CDATA[<p>You can map and fold in Zig but lack of closures makes it unergonomic. Here's a completely silly example that shows Zig type hackery as well as memory management. </p><pre><code class="language-Zig">const std = @import("std");
const mem = std.mem;
const testing = std.testing;

const Allocator = mem.Allocator;
pub const Error = Allocator.Error || error{};

pub const Expr = union(enum) {
    binary: Binary,
    int: usize,

    pub const Binary = struct {
        op: BinaryOp,
        lhs: *Expr,
        rhs: *Expr,
    };

    pub const BinaryOp = enum {
        @"+",
    };
};

pub fn exprWalker(T: type, E: type) type {
    return struct {
        context: T,
        walker: Walker,

        const Self = @This();

        pub const Walker = *const fn (*Self, *Expr) E!bool;

        pub fn run(self: *Self, node: *Expr) E!void {
            if (try self.walker(self, node))
                return;
            switch (node.*) {
                .binary =&gt; |binary| {
                    try self.run(binary.lhs);
                    try self.run(binary.rhs);
                },
                else =&gt; {},
            }
        }
    };
}

fn sumup(node: *Expr) Error!usize {
    const Context = usize;
    const Walker = exprWalker(Context, Error);
    const walk = struct {
        fn walk(self: *Walker, expr: *Expr) Error!bool {
            switch (expr.*) {
                .int =&gt; |i| {
                    self.context += i;
                    return true;
                },
                else =&gt; {},
            }
            return false;
        }
    }.walk;

    var walker = Walker{
        .context = 0,
        .walker = walk,
    };
    try walker.run(node);
    return walker.context;
}

pub fn clone(alloc: Allocator, value: anytype) Error!*@TypeOf(value) {
    const result = try alloc.create(@TypeOf(value));
    result.* = value;
    return result;
}

pub fn main() !void {
    const page_alloc = std.heap.page_allocator;
    var arena = std.heap.ArenaAllocator.init(page_alloc);
    defer arena.deinit();
    const alloc = arena.allocator();
    const lhs = Expr{ .int = 1 };
    const rhs = Expr{ .int = 2 };
    var expr = Expr{
        .binary = .{
            .op = .@"+",
            .lhs = try clone(alloc, lhs),
            .rhs = try clone(alloc, rhs),
        },
    };
    std.debug.print("sum = {d}\n", .{try sumup(&amp;expr)});
}
</code></pre><p>Let's take it apart...</p><p>Recursive data structures require pointers so both left-hand side (<code>lhs</code>) and right-hand side (<code>rhs</code>) of the expression need to be pointers! Also, <a href="https://zig.guide/language-basics/unions/" rel="noreferrer">tagged unions</a> (see union enum) are analogous to <a href="https://dev.realworldocaml.org/variants.html" rel="noreferrer">OCaml variants</a>.</p><pre><code class="language-Zig">pub const Expr = union(enum) {
    binary: Binary,
    int: usize,

    pub const Binary = struct {
        op: BinaryOp,
        lhs: *Expr,
        rhs: *Expr,
    };

    pub const BinaryOp = enum {
        @"+",
    };
};</code></pre><p>Memory allocation in Zig is very much explicit! I started my compiler project manually allocating and deallocating everything but quickly went with the <a href="https://zig.guide/standard-library/allocators" rel="noreferrer">arena allocator</a>. This lets me free up all the data structures in one fell swoop once the compiler exits. <a href="https://zig.guide/language-basics/defer" rel="noreferrer">Defer</a> is awesome and will run when we exit the current scope. Think RAII in C++.</p><pre><code class="language-Zig">const page_alloc = std.heap.page_allocator;
var arena = std.heap.ArenaAllocator.init(page_alloc);
defer arena.deinit();
const alloc = arena.allocator();
</code></pre><p>This handy little function will make a copy of any value given to it by first allocating a chunk of memory the size of the given value and then assigning the value to the "dereferenced pointer". <br><br>Zig duck typing is great! Notice that I'm not specifying the type of value (<a href="https://reddit.com/r/Zig/comments/11pxp5m/help_a_noob_how_does_anytype_work/" rel="noreferrer">anytype</a>) and telling the Zig compiler to use the type of the value given ( <code>TypeOf(value)</code>)</p><pre><code class="language-Zig">pub fn exprWalker(T: type, E: type) type {
    return struct {
        context: T,
        walker: Walker,

        const Self = @This();

        pub const Walker = *const fn (*Self, *Expr) E!bool;

        pub fn run(self: *Self, node: *Expr) E!void {
            if (try self.walker(self, node))
                return;
            switch (node.*) {
                .binary =&gt; |binary| {
                    try self.run(binary.lhs);
                    try self.run(binary.rhs);
                },
                else =&gt; {},
            }
        }
    };
}
</code></pre><p>Zig comptime generics at work here. <br><br>The <code>exprWalker</code> function returns a new type parameterized on the context and error types supplied to it. <br><br>The <code>Walker</code> type is a function that will do the heavy lifting. The expression walking machinery will abruptly stop if the function tells it that this expression has been handled. Otherwise, It will happily recurse into nested expressions. <br><br>Zig pattern matching using <a href="https://zig.guide/language-basics/switch/" rel="noreferrer">switch</a> is not <a href="https://dev.realworldocaml.org/lists-and-patterns.html" rel="noreferrer">as advanced as in OCaml</a> but perfectly serviceable. Finally, <code>{}</code> is an empty block.</p><pre><code class="language-Zig">fn sumup(node: *Expr) Error!usize {
    const Context = usize;
    const Walker = exprWalker(Context, Error);
    const walk = struct {
        fn walk(self: *Walker, expr: *Expr) Error!bool {
            switch (expr.*) {
                .int =&gt; |i| {
                    self.context += i;
                    return true;
                },
                else =&gt; {},
            }
            return false;
        }
    }.walk;

    var walker = Walker{
        .context = 0,
        .walker = walk,
    };
    try walker.run(node);
    return walker.context;
}
</code></pre><p>Here <code>const Walker = exprWalker(Context, Error);</code> instantiates a new Walker that's parameterized over the <code>Context</code> and <code>Error</code> types. <br><br>The walk function then looks for ints and sums them up, storing the sum in the context. <br><br>There are no closures in Zig but you can sort of kind of emulate them by returning a struct with a function inside it. </p><p>That's it!</p>
]]></description>
    </item>
  </channel>
</rss>
