Type to search.

    This is Matt Widmann's site for working notes and articles. I'm a programmer so I tend to write about software, but I also love nature, bicycles, and writing.


    1. Generating this site on Zig 0.16

      These are some rough notes on updating a small project for Zig 0.16. There are bound to be misunderstandings and general confusion, since I'm not taking a disciplined approach to learning the language or keeping up with its roadmap.

      The program that generates this site is written in Zig11 I rewrote the program from Rust because I prefer Zig's minimalism and it seemed like a fun challenge., a programming language that's still undergoing breaking changes with each new version. Before I was finished with the rewrite, version 0.15 caused Writergate. This changed how the language provided file descriptor-based reading and writing. The new interfaces require a buffer, explicit flushing to drain that buffer, and care to always pass a pointer to the writer structure for downstream clients, instead of copying it. That last point caused one of the more painful debugging sessions I've had with Zig22 Not helped because lldb was inexplicably non-functional on my machine at the time..

      feat: update to Zig 0.15
      8 files changed, 213 insertions(+), 211 deletions(-)
      

      In April 2026, Zig updated to 0.16 and revamped its standard library's approach to handling I/O. Functions now create or observe side effects outside of the the currently-executing CPU's register file and memory33 Globals and stack memory are allowed implicitly, but the heap's side effects use a std.mem.Allocator to manipulate. by accepting a std.Io argument. This includes file or network I/O, entropy and random numbers, synchronization primitives, and even reading a monotonic clock. The effect of this new requirement on my site was significant44 The commit prefix I used here compared to 0.15 reflects the slog this was.:

      chore: update to Zig 0.16
      10 files changed, 542 insertions(+), 518 deletions(-)
      

      Most of the changes just required plumbing std.Io to all the functions that needed them.

      Zig tooling

      It still took me a few hours to update the site by hand. The most frustrating part of the process is the language server not showing errors and the compiler only showing the next reachable error. I'm not sure why the language server couldn't detect obvious errors, like incorrect standard library interfaces. The compiler's friction is due to an explicit design decision: it only compiles code that is used.

      A snippet of Zig source being edited in Helix with an error on the `std.debug.print` line, but the language server only shows an earlier error.
      The debug print here takes two arguments, but only the unused variable error was shown by zls.

      In practice, this means you need a persistent split window to run zig build --watch alongside an editor. Before language servers, I would use vim's quickfix support for this or maybe even Acme's right-click to go to a line. But in the last five years, I've gotten used to a language server showing diagnostics. In Helix, you can list them all with <space>d and move between them with [d and ]d. But there's no support for feeding the output of a compiler into this UI. So I stuck with the --watch approach.

      A terminal window with three split sections: the left hand side is an editor showing Zig source code and the right-hand side has the output of the tests and run output.
      My terminal set up for iterating on changes to the site generator.

      It would be lovely if each Zig release came with a source-to-source translation tool of some kind, like Hare often includes.

      Multithreading

      A reason for all of this pain is to provide a swappable runtime for Futures and async/await in the standard library. My site generator, despite being almost completely I/O bound, was single-threaded as it waited for Zig's concurrency story to be more fleshed out. But adding concurrency here was significantly more difficult than the previous iteration of the site written in Rust. In Rust, I think I just adopted Rayon's par_iter to process notes, slapped on a Mutex where the compiler told me there was shared mutable state, and then the change worked.

      Zig is much looser with temporal and data race safety, so I had to take more care. The closest thing I could find to "generate a bunch of work that should happen concurrently" was a std.Io.Group. I reworked a few of the algorithms to not rely on state that would need to be shared. For instance, indexing words needed to add them to a singular hash table during Markdown conversion. In the multithreaded code, each note gets its own hash table, which are merged together once all notes are processed. I decided to use a lock on the diagnostics list, since those should be much rarer.

      The std.Io.Group code I wrote looks like this:

      var group: std.Io.Group = .init;
      for (0..self.notes.items.len) |i| {
          group.async(io, processAsync, .{
              io,
              allocator,
              self,
              &processing,
              i,
              notes_dir_in,
              notes_dir_out,
              index,
              diagnostics,
          });
      }
      try group.await(io);
      

      Where processAsync is just a wrapper around process that can only fail with error{Canceled} and hides any underlying errors with catch unreachable. Clearly not production-quality code, but I was too lazy to add an out-parameter for the actual errors.

      Despite that, these hacks did give an immediate speedup to the end-to-end latency (this is from the debug build):

      notes:  97 ( 400.239 KiB)   37.078 ms ( 73.1%)
       / md:  97 ( 400.239 KiB)   67.998 ms (183.4%)
      

      This is saying that overall notes processing took 37ms, but the total time spent working on Markdown files was 67ms, indicating a bit of parallelism. But my computer has 8 CPUs, not 2.

      std.Io.Group is probably the wrong primitive here. Looking at the trace in Instruments, the main thread can "eagerly" take on work to execute (running the passed-in function) if all of the other CPU's threads are still working on a function. Because the loop around notes is responsible for enqueuing new work items, this can starve those threads for milliseconds. I'm not sure why this behavior exists when group.await could just as well start pulling work items without interfering this way. And using group.concurrent instead of group.async just overcommits dozens of threads which each get their own tiny slice of CPU.

      A screenshot of the performance analysis tool Instruments showing multiple threads in the site generator blocked waiting for work.
      Instruments showing threads blocked waiting for work from the main thread.

      What I need here is an async I/O implementation that takes a batch of operations to execute and does work-stealing. In Swift or C on macOS, I would reach for Dispatch.concurrentPerform, which is still not a great answer for asynchronous I/O. I'm not sure this is what std.Io.Batch is meant for, despite the name. I could simulate this with a pipeline that puts the Markdown conversion in between two std.Io.Queues for Markdown text and then HTML to write. At these small durations, I'm wary of the overheads eating into the useful work being done. Or maybe std.Io.Select is the right way to ensure the operations don't interfere with enqueuing work. The documentation around these isn't very clear to me from just reading the standard library reference.55 If anyone with more experience with Zig's concurrency primitives has a path I could take, I'd love to hear from you.

      In any case, I applied this approach to a few other places in the generator that seemed like they would benefit:

      • 3ms: Writing the backlinks on each note66 Backlinks are the notes that link to the current note and show up at the bottom of each note. in parallel made that go from ~5ms to ~2ms.

      • 0ms: Stemming77 Stemming is where full words are converted into a form that preserves their meaning but avoids unnecessary uniqueness. This includes removing the trailing "s" for plural nouns, among (many) other heuristics for English. for the index showed up as an expensive single-threaded task, but moving that into the word writer (during individual note processing) didn't help.

      • 5ms: 7ms of index writing went down to around 2ms when done in parallel.

      • 2ms: Converting pages (like the colophon) and copying static assets in parallel helped by a couple milliseconds.

      The site originally generated in ~45ms on my MacBook Air M2 and now finishes in just under 20ms. I should be able to get this to sub-10ms with the right concurrency design, but at this point there aren't any easy wins left.

      Despite my initial stumbles, I'm still really excited for the future of concurrency in Zig!

    2. Updates for April 2026

      Here's what I've been working on, doing, and thinking about this month.

      • I upgraded this site's generator for Zig 0.16 and made it multithreaded.

      • I went on a business trip to London for two weeks, staying in the central part of the city, close to the Tower Bridge. While I worked during the week, I still had four days on the weekend for sightseeing in the city and trips to Cambridge and Bletchley Park11 This is where the ciphers used by Axis powers in World War II were broken using early computers..

      • A notebook that lays flat was my first photo-rich article. Despite the low-quality photos, it's nice to share what my setup looks like.

      I did not read nearly as much as I did last month, but I did find time for one fun science-fiction book:

      Instead, I read some excellent articles:

      • Go Ahead and Use AI. It Will Only Help Me Dominate You.

        This was hilarious and a testament to writing in your own voice without the help of LLMs.

      • Your hex editor should color-code bytes

        I have some strong opinions about syntax highlighting and this is a neat application of it. I'd like a hex editor with structural grouping for known-formats, since I almost always use them on file data. But this looks really useful for hex editing arbitrary data.

      • A tail-call interpreter in (nightly) Rust

        I wish there was some way to apply this to the work I do in Swift, where I built a little arithmetic expression evaluator for the CPU Counters instrument. To be fair, I haven't actually looked at the generated code and I'm likely leaving a lot of performance on the table. Byte code VMs are a really effective solution to tricky problems and can be competitive with native code in some cases.

        Andy Wingo wrote the value of a performance oracle as a follow-up, showing that his WebAssembly runtime isn't as slow as Wasmtime. I'm still not convinced that there are not fundamental limits to performance, having re-read the WebAssembly Troubles series recently. And as much as I love the Uxn ecosystem, his take that a Uxn interpreter is a viable target to optimize for is a little odd. There are veritable loads of more commonplace workloads available in languages that can target WebAssembly.

      • Porting Mac OS X to the Nintendo Wii

        Seriously impressive. This might be the one of the last pieces of greenfield IOKit development to happen outside of Apple.

      • 256 Lines or Less: Test Case Minimization

        This is a nice demonstration that you don't need to be too fancy to get 80 to 90 percent of the benefit of an idea. The problem I've faced with property-based testing is finding useful invariants for a data structure or algorithm that can be easily checked. But I also didn't have access to convenient generation libraries.

      • Lua can be a really cool HTML templating engine

        I love a tactical Lua interpreter as much as the next person, but I'm not convinced that a DSL for HTML is a great application for it. Regardless, I appreciated how the code was presented and discussed. It's almost like a literate program, but positioned as a persuasive piece.

      Before my trip to London, we put on a little Easter egg hunt for my daughter in our backyard and, the next day, another one at my aunt's house. My aunt cleverly assigned each kid an egg color so they all got a set number of their own.

      While I was in London, I experienced a busy, built-up, very old, dense city and got around almost exclusively on their underground light rail and regional heavy rail systems. Getting from Heathrow airport to the middle of the city was fast and relatively convenient: the Elizabeth line was busy but not too bad. Being able to tap-to-pay everywhere took the stress away from finding a kiosk or getting some London-specific card.

      Now that I'm back home, I'm taking a week off work to spend time with my family and do some spring cleaning.

    3. A notebook that lays flat

      I'm a fan of computers. Taking notes on them brings a lot of benefits: easy backups, syncing across devices, search, and invisible revisions. But for thinking through problems and tracking my daily thoughts, I've recently moved away from using note taking apps11 Like iA Writer or Obsidian..

      Writing by hand helps me organize my ideas and avoid the tendency to get bogged down in revisions. Handwritten notes in meetings aren't susceptible to a computer's distractions. Using a pen, they're also immutable and capture how your thoughts evolve over time.22 To bring that to my computer notes, a script commits my notes to version control every 5 minutes. But it's not the same and way less legible. It's also fun for me to work on my handwriting and flip through pages of a well-used notebook. That's probably the biggest reason I've switched: I just like them more.

      A notebook with dot grid pages open at an angle with a narrow focus on handwritten text about journaling.
      How I'm starting drafts now (ignoring typos).

      I've gone through a lot of notebooks that don't work well for me:

      • Hardbound notebooks (e.g. Moleskine and Leuchtturm) are fine for the first dozen pages but soon stop laying flat and require "breaking" the binding to work the book in.

      • Spiral-bound notebooks (e.g. Marumans Mnemosyne) lay flat but their wires get bent and caught up in a crowded bag. Because there's play in the holes for the wires, pages on the inside can get bent on the edges when the cover is offset.

      • Disc notebook systems (e.g. Circa) have all the drawbacks of spiral bound except they're very durable. I used these in college and generally liked them because they were cheap to refill and I needed a full letter size page for taking notes with diagrams.

      • Notebooks with no spine (e.g. Field Notes) don't lay flat and can't fit much paper in them, but they're very portable.

      However, I finally found a style of notebooks I'm happy with. I bought a JetPens Kanso Noto at the end of last year and started using it as a journal. This is a lay-flat notebook with a mostly exposed spine: instead of a continuous cover, there's only some tape to hold the two cardstock covers together.

      A dark grey notebook with an embossed logo on the front and see-through square grid tape on the spine.
      The Kanso Noto in charcoal grey.

      A5 notebooks are the best size for me. There's enough room on the page for readable paragraphs at my typical handwriting size. It's also portable enough to throw in a small bag or front pocket of a backpack.

      I use a LAMY Safari fountain pen with a fine nib and De Atramentis Document ink in black. The Kanso's Tomoe River paper is pretty thin but handles it well. There's a little ghosting on the other side of pages, but it doesn't hurt legibility. Another color can be used for emphasis and makes it easy to spot edits. The cyan color of this ink bled through badly, which was a shame, but black and red are fine.

      A two pen cozy surrounded by black and red fountain pens and their respective ink bottles.
      The writing tools I use.

      It's not as durable as a hardbound notebook, so I eventually wrapped it in a BKxAP Canvas Cover33 I guess this would address my issues with spiral-bound notebooks, too.. The canvas is soft enough to still let the notebook lay flat. It helps keep the sides of the notebook a little more protected since its own cover is exactly the same size as the pages. With thick seams pushing underneath the thin pages, I had to add a clear writing board to make it easier to write on the first and last pages of the notebook.

      A blue canvas covered notebook.
      The Kanso Noto in its cover.

      When I fill this up44 After 2 months I've used about a third of it., I'm excited to try two other options, in this order:

      • Pith Supply Yuzu55 Unfortunately this isn't readily available in the US.: Steven Schultz on YouTube claims this is the perfect lay-flat notebook and the colors look really nice.

      • Midori MD Notebook: It has these little perforated tabs in the corners to mark your location in the book without a fabric bookmark.

    4. Updates for March 2026

      Here's what I've been working on and thinking about this month.

      • I renamed the "now" series to just "updates," to catalogue what's happened each month.

      • Index pages (lists of articles) contain full article content inline instead of just the title and a link to the article page.

      • I added a human.json with a single entry for my old college friend.

      • The 2025 Priority Gemini bicycle's shift cable disconnected and I had to commute home in a pretty low gear.

      • I swapped out wallets in my Everyday carry.

      I read a few fiction books at the beginning of March after getting all the way through The Lord of the Rings for the first (and probably last) time:

      And read some articles but unlike books, I don't diligently track all of them:

      • Buc-ee's Is Better at Placemaking Than Your City by Max Mautner

        This piece was uncomfortable but in the best way. The urbanist movement needs more humility and meeting people where they are.

      • Good trains by Robin Sloan

        Not much to takeaway from this one but I love Robin's writing and enthusiasm.

      • GitButler CLI Is Really Good by Mat Duggan

        I'm all in on Jujutsu these days but only started looking around because I detested Git's default one-line graph log ASCII art11 Taste in command line tools matters a lot to me, I guess?. This looks so good that I probably wouldn't have made the switch if this was available at the time.

      • The Most Dangerous Line: Behind the Hawker stall test crashes by Admiral Cloudberg

        A haunting article mixed in with detailed descriptions of organizational, engineering, and human failings.

      We started the month recovering from a debilitating cold. When we were on the other side of it, we took our daughter to the beach22 Where she fell over whenever the waves lapped against her legs.. As the heat wave descended, I set up a water table and sand table for her to play with in the backyard.

      The garden had lost some plants over the winter, so we went to our local nursery and picked up a few flowers, shrubs, vegetables, and herbs to get things back on track. I'm pretty excited about the little vertical herb garden we're starting, currently with multiple varieties of thyme, sage, and chives.

      We capped off the month with a road trip up to Sacramento for a stay in Folsom and visit with family.

    5. Single letter commands

      If I use a shell command often, I'll condense into a short, single letter alias as a mnemonic. I've settled on a few key aliases:

      • l: ls, list the contents of a directory

      • e: whatever terminal editor I've latched onto

      • f: fg, bring the last backgrounded job to the foreground

      • j: the Jujutsu version control system

      • c: switch the current directory to a source repository via fzf

      These act like a command layer for the shell, similar to keyboard shorcuts for apps or the terse commands of vi. They're mapped as abbreviations, usually, which expand to their full command when invoked. When I communicate using my terminal scrollback history, any well-known commands involved are visible.

      I've also recently started naming my personal scripts with single letters:

      • h: start a new journal entry with the current date and hour populated

      • n: start an editor in my notes directory

      • d [<title>]: with an argument, create a new draft, or without, start an editor on the drafts directory

      To me, this feels faster and more precise than typing out a longer name.