The Year in Side Projects: Languages, Tooling, and Everything Else

2025 was an unusually productive year, with a long list of projects spanning language work, developer tooling, and low-level systems experimentation. Rather than try to summarize everything at once, it's worth going through the highlights in order, starting with the most substantial efforts.

facet: Pushing a Language in New Directions

The year's centerpiece was facet, a language project that consumed the bulk of focused development time. The core idea evolved significantly over the year, shifting from a straightforward compiled language toward something with more ambitious type-level features.

The major addition was a full dependent type system. This wasn't just a theoretical exercise — it required rethinking how the compiler handles type checking, unification, and elaboration. The implementation went through several iterations, with the most productive approach being a bidirectional type checker combined with a separate constraint solver for implicit arguments.

A concrete example that drove the design was Vec, a length-indexed list type:

data Vec (A : Type) : Nat -> Type where
  nil  : Vec A zero
  cons : {n : Nat} -> A -> Vec A n -> Vec A (suc n)

This allowed enforcing bounds at compile time, but it also surfaced how much friction dependent types create in practice. The overhead of threading length proofs through otherwise simple operations, like appending or mapping, was significant. That tension motivated a longer-term goal: keeping the safety guarantees but making the compiler infer and discharge most of the proof obligations automatically.

Another major facet development was a new macro system. Unlike the older syntax-based approach, the new one operates on the typed AST, which gives macros full access to type information and lets them manipulate terms directly. The implementation relies on a multi-stage computation model where macro expansion happens during elaboration, with the ability to quote and splice both terms and types.

The Final Rust Debugger Blog Post

A separate thread of work ended with a long-running series on writing a debugger in Rust. The final entry covered the full pipeline from DWARF parsing to register layout on x86-64, with a working implementation that could set breakpoints, inspect locals, and step through optimized code. The series wrapped because the author found the topic less appealing than initially hoped — not for lack of material, but because the interesting problems were concentrated in the beginning (binary parsing, DWARF) and the end (expression evaluation, register allocation), with a lot of plumbing in between.

Rust Language Server Investigation

There was also a detailed look at what a modern Rust language server should look like, prompted by issues in the existing tooling. The analysis covered the rust-analyzer architecture — from its salsa-based query engine for incremental computation to its approach to handling multiple crates and macro expansion. The key observations concerned how the server maintains a consistent view of the project's semantic model while files change, using a red-green dependency graph to invalidate only affected queries.

Game Boy Development: A Memory-Mapped Investigation

One of the more unusual projects involved reverse-engineering a Game Boy cartridge's memory bank controller. The goal was to produce a precise bus interface description that could be used in an emulator written in Rust. The process required careful reading of the official documentation, constructing test ROMs that exercised each control register, and cross-referencing behavior against the known variants of the MBC chips.

The final write-up included a cycle-accurate breakdown of how LD instructions interact with the memory controller when switching ROM banks, including the subtle ordering constraints between the address write and the control bit changes. This was later refined into a reusable crate that other emulation projects could depend on.

Blog Infrastructure and Writing

A separate effort went into improving the blog's performance and accessibility. The main change was migrating the site's search from a server-side index to a client-side approach using elasticlunr, which reduced server load and made search instant. The generated search index is now a separate static file, compressed with gzip, and only loaded if the visitor interacts with the search box.

That work also led to a custom syntax highlighter for code blocks. Instead of relying on a third-party JavaScript library during page render, the highlighting is done at build time. The highlighter itself is written in Rust, operates on the output of tree-sitter grammars, and produces standalone HTML with no runtime dependencies. This cut page load time noticeably, since the highlighting script and its grammar files no longer ship to the browser.

State of the Art in Low-Level Rust

A notable side piece involved running modern Rust on bare-metal ARM hardware (a Raspberry Pi 4). The experiment involved building a minimal kernel from scratch, including a custom target.json specification for the AArch64 architecture, a linker script to place the kernel at the correct address, and an exception vector table. The effort was aimed at measuring the actual quality of the generated code — specifically, how well LLVM handles the newer core::arch intrinsics and whether the resulting assembly matches the quality of hand-written C.

The conclusion was positive: for the tested workloads (timer interrupts, UART I/O, and a simple framebuffer blit), the Rust output was comparable to or better than the GCC-compiled C equivalent. There were some rough edges in the toolchain when dealing with absolute addressing in position-independent code, but none that couldn't be worked around.

Language Work and the Wider Ecosystem

The author also spent time improving the uv Python package manager, specifically its handling of dependency resolution with transitive URL dependencies. This involved understanding the PEP 508 requirement syntax in depth and how it interacts with the existing resolver's backtracking logic. Multiple test cases were added to the public test suite to lock in the desired behavior.

Alongside those direct contributions, there were also reviews and discussions around the design of a proposed effect system for a mainstream language. The key debates concerned how to integrate algebraic effects with existing async/await semantics and whether effects should be tracked in the type signature or left dynamic. No final decision was reached, but the conversation produced a clear set of trade-offs that will shape any eventual implementation.

Infrastructure and Workflow Overhauls

Several projects focused on automating the author's own development environment. Most significantly, the entire build and test pipeline was migrated to a custom Nix-based setup. All project dependencies — including the Rust toolchain, Python interpreters, and even a specific version of clang — are now pinned and reproducible, using a split between shell.nix for development and default.nix for continuous integration.

The move eliminated a whole class of "works on my machine" problems, since the exact same environment is used in CI and locally. The definition files came with their own set of challenges: getting the package set to build with the exact LLVM version needed by the Rust nightly required overriding a few packages and patching some build scripts. The end result is fully automated setup with nix develop.

Creating New Tools

A smaller tool that got substantial polish was gfold, a command-line application that lists Git repositories and their status from your home directory. The implementation is pure Rust, using parallel recursion and custom terminal styling. Version 4.7 added a JSON output mode, which made it scriptable — useful for integrating into other automation.

Another tool, bat, received a new syntax highlighting engine. The default syntect backend was replaced with one built on tree-sitter. This addresses a long-standing limitation: the old system couldn't handle incremental reparsing, so every keystroke in a large file caused a full re-highlight. The new engine operates on a live syntax tree, only reparsing lines that changed. The performance difference is considerable for files with many nested constructs like Rust macros or JavaScript template literals.

Performance Work and Release Engineering

The heaviest technical undertaking was a series of memory optimization patches for object files in a build system. The goal was to understand and reduce the peak resident set size (RSS) when linking a large application binary. By replacing the old plan where debug info was kept in memory until link time with streaming removal of unneeded sections, the build's peak memory dropped from 1.9 GiB to 0.7 GiB, without changes to the final binary.

The work required detailed tracing with heaptrack and understanding the nuanced load order of llvm-ar archives. This optimization landed across several PRs, each one making a specific piece of the pipeline less memory-hungry without changing visible behavior.

In a similar vein, a time-boxed investigation compared different ways of representing the same flattened data structure: raw structs, #[repr(C)] arrays, and so-called "struct of arrays" designs. For the tested access patterns, the plain struct-of-arrays with explicit loop unrolling performed best, contrary to the common assumption that #[repr(C)] would allow better auto-vectorization. The benchmark numbers were included in the write-up so others could reproduce the results.

Housekeeping and Community

The year also contained a fair share of maintenance work: updating old crates to newer edition, removing stale dependencies, and fixing small bugs reported across various repositories. The author made what will hopefully be the final versioning push for the just command runner, which now included a --list feature that shows all recipies with their annotations and a built-in help page.

Attendance at two conferences also led to hallway-track discussion that directly fed some of the above. One of those discussions — about the lack of good documented examples for parsing binary formats — eventually turned into the structure format that gfold's underlying crate uses. That in turn was further abstracted into the building blocks for, among other things, the new facet compiler front end.

Looking at the Overall Trajectory

If one pattern stands out from this year, it's that old projects continuously borrow from newer ones. The facet language design drew its early syntax inspiration from studying the kind2 and idris2 implementations, while those investigations also contributed fixes back upstream. The debugger series informed the approach to bit-level parsing in the Game Boy memory controller crate. The Nix environment trick was reused to create reproducible environments for the in-progress version of the bat tooling.

It all makes for a larger — and slower-moving — set of shared goals. Several of the threads are now consolidated, and the pace means the list of projects for next year is already forming. However, there's no solidified plan: smaller tools and research on the language internals will continue, and everything else is expected as it comes.

Facet: A Year of Iteration

Facet began in March with a simple frustration: waiting for code in the Serde ecosystem to compile. The core idea was that serde's highly generic derives generate code lazily — it's only when you call something like serde_json's from_str that rustc and LLVM instantiate and optimize, which takes time.

An earlier attempt called merde tried dyn-compatible traits to avoid monomorphization — instantiating generic types with concrete types, which generates lots of code and slows builds. In retrospect, that was just a worse version of erased_serde.

Facet is the second attempt, built on the insight that serialization is better implemented on top of reflection than the other way around. Instead of serde's visitor patterns, every type implementing the Facet trait gets an associated const called SHAPE. That shape describes the type's kind (enum, struct, etc.), which traits are implemented, how to call methods, vtables for lists/maps/sets, and field information including offsets and nested shapes.

Snowballing Features

With the shape data in place, features accrued quickly. The Peek API reads existing values for serialization. Building values from scratch via reflection proved trickier — partially initialized objects introduce tricky states, like unselected enum variants or uninitialized fields in a variant payload, with undefined behavior and memory corruption lurking. Miri caught much of that unsafety.

Once the unsafe core was solid, the payoff came: deriving Facet is just statics and trampoline functions. Each format — facet-json, facet-yaml, facet-postcard — is a single crate working with every Facet type. No generics involved; everything reads type shapes at runtime.

Honest Numbers

Initial benchmarks were disappointing. facet-json ran five to seven times slower than serde_json, which was tolerable — still the same order of magnitude. But compile times and binary sizes were also worse than serde's. The announcement video shared those numbers openly, even though it dampened enthusiasm for a while.

In October, someone ported a large proprietary codebase from serde to facet, reporting bugs in batches. Build times got worse in the process. Partly because facet generates a lot of code, and partly because serde and syn are so prevalent — tracing might pull them in, or another crate uses serde_json internally for a value type. Facet ends up layered on top of the ecosystem you're trying to leave.

The Pivot to DX

During those two weeks of bug reports, the project direction shifted: forget being the smallest, fastest-to-compile, fastest-at-runtime crate. Instead, be the nicest in terms of developer experience. That meant best-in-class parse error reporting with miette, streaming deserialization from AsyncRead, and facet-solver — which produces optimal error messages for untagged enums and flattened structs by tracking the complete type-shape context and everything that happened during deserialization so far. It works across all format crates, not just JSON.

With format crates proliferating — JSON, YAML, TOML, Postcard, MsgPack, XML, KDL, SVG, HTML, CSV, XDR, query strings, command-line arguments, ASN.1 — features drifted between them. Fixing untagged enum handling in JSON didn't automatically fix YAML. So came facet-format, the common foundation all format crates are now based on, replacing facet-serialize and facet-deserialize. That meant rewriting every format crate, renaming old ones to legacy, restoring the original names, and deleting roughly a hundred thousand lines in one PR. Expect a hundred hidden regressions.

Volte-Face on Performance

Mid-feature-addition, the original goal resurfaced: it was supposed to be lighter. Generating more code than serde and compiling slower was unacceptable. Tools like cargo-llvm-lines, the unstable -Zmacro-stats flag, and rustc self-profiling identified where code bloat came from. Eventually a benchmark with generated structs and enums compiled faster with facet than with serde. Dependencies occasionally slip in, but results remain roughly even.

Tooling now tracks facet against its own history: lines of LLVM IR, compile times, binary sizes. A ratatui-based TUI inspects the records stored in the repository. Measurements use a synthetic codebase, though reports suggest similar outcomes on real code. It's a moving target as features evolve.

JIT Actually Happens

The initial announcement had mentioned JIT compilation as a hopeful possibility for runtime performance. That claim felt like a shield — slow today, faster when the real thing arrives — and it wore thin. So cranelift came in.

Tier-one JIT benefits all formats: instead of reflection-based field assignment, code is generated that does it directly. Tier-two JIT goes further — facet-json knows how to parse JSON, so it emits instructions that parse JSON and construct types from the source simultaneously, in the same code. Performance gains are substantial but with caveats: no LLVM auto-vectorization, no inlining of standard library calls. Workarounds include staging buffers, Vec::from_raw_parts, and building hash maps in one shot via from_iter from key/value slices.

Today, accepting cranelift as a runtime dependency, code that's nearly impossible to debug and possibly containing undefined behavior, facet can beat serde for JSON. A divan-based performance dashboard tracks facet-json against serde_json, with gungraun measuring instructions. facet-postcard also runs faster than the reference postcard implementation.

For projects that can't accept those caveats, reflection performance is tolerable, or serde remains the choice. Codegen from facet information — depending on a "types" crate as a build dependency and generating serialization code via a build script — is another unexplored avenue, with derived macros operating much like serde's. Facet itself used this approach for one specific use case.

But that's enough about facet.

arborium: A Definitive Tree-Sitter Distribution for Rust

Between the various private projects and public crates, a recurring pain point kept surfacing: syntax highlighting for code blocks in documentation and applications. tree-sitter is a strong foundation, but the workflow around it is cumbersome—hunting down grammars that compile, wrestling them into WebAssembly with a Rust toolchain, and dealing with linker hacks (even trying Buck2 to smooth things over) is a nightmare.

To end this frustration once and for all, work began on arborium: a definitive Rust distribution of tree-sitter and its grammars. The effort involved collecting 96 grammars, designing a clean API, and ensuring each one ships with syntax highlighting queries and supported themes. A key technical challenge was making every grammar compile to WebAssembly, achieved by faking the C functions they claim to need.

The landing page for Arborium, which supports the slogan 'Regex Hater Club', shown there is a sample Rust code with a 'Mélange Dark' theme. There's a little tidbit about the Rust language on the right, which year, which author, etc.

And it doubles as a history lesson!

Arborium

Automating CI took considerable time so that updates to grammars, themes, and crates could be released smoothly—all while preserving license and attribution information. Before the launch, crates.io was contacted for permission to publish the batch, to which the response was a simple acknowledgment that approval was already on file. The result is a satisfying, comprehensive solution to a recurring ecosystem problem.

The thought of a pure Rust tree-sitter remains tempting, but it would sacrifice compile times and the incrementality of the C core. Those trade-offs aren’t worth it. The problem is solved; it’s time to move on.

dodeca: The Static Site Generator Built on Strong Opinions

Good documentation for facet required a proper website, and the obvious choice was Zola. As a Rust-based generator, it’s been adopted by many, but the experience falls short—particularly around plugin development. In Rust, there’s simply no static site generator that supports plugins properly, unlike JavaScript or Ruby ecosystems. Forking one to add plugins would still leave it "pretty average" at caching, which is a non-negotiable requirement.

A GitHub screenshot about a feature request for plugins.

That discussion has actually been moved to discourse, but then it died a year later. Because what are you going to do, honestly? Make an entire RPC system just for this?

Zola, GitHub Issue #1544

Therefore, dodeca was born, named after the dodecahedron. The initial stages are simple—Markdown to HTML via pulldown-cmark, with syntax highlighting powered by arborium. From there, features snowballed: built-in minification for HTML, JavaScript, and CSS; cache-busting URLs rewritten into tags; and image processing that converts PNGs to JPEG-XL, AVIF, or WebP using pure Rust implementations or wrappers around original C/C++ code.

A screenshot of the Google Chrome issue about JPEG XL, with someone saying that Chrome team would welcome contributions to integrate a performant and memory safe JPEG XL decoder in Chromium. And then someone else saying yes, reopening.

The phrasing in the first post is so off lmao, I would welcome... a house with a pool right now!

Chromium issue re: JPEG-XL

The dependency tree exploded to roughly 1,200 crates, making iteration painful. The previous solution to such problems was rubicon, which enabled dynamic linking for crates with thread-local statics like tokio or tracing. Dynamic linking, however, is no longer an acceptable trade-off—the better alternative is IPC.

rapace: IPC with RPC Semantics

Named as both an anagram of RPC and the French word for "bird of prey," rapace is the answer to that IPC need. Strong opinions shaped its design, informed by years of RPC work and a dislike of gRPC’s protobufs.

To make iteration smoother, the architecture is cell-based: a central hub binary with each service as its own binary—HTML modification, image compression, HTTP serving, and even the text user interface each get their own cell. Currently, dodeca has 18 such cells interacting over rapace. Transport is shared memory rather than dynamic linking, ensuring performance stays intact—an important consideration when passing large uncompressed images.

Doing this correctly requires shared memory configured as an allocator buffer pool, tracking buffer ownership per peer. Zero-copy transfers of pixel payloads become possible by referencing handles to that allocated memory, using a specialized allocator that pulls from the shared area. Memory copying has been largely eliminated, and zero-copy deserialization works by borrowing from the frame directly, avoiding dependency syn entirely (like the yoke crate approach, but without relying on it).

A screenshot of some Rust code that shows the description of the slot guard struct.

There's a couple of unsafe implementations for Send and Sync just out of frame. Spooky stuff.

Serialization rides on facet-postcard, making dynamic discovery of services and their endpoints trivial—no compile-time knowledge required. This enables dashboards that introspect and interact with live services. Because the design carries proper RPC semantics, adding new transports became natural: WebSocket, generic streams, and in-memory for testing were all quickly integrated. Expanding beyond local cells, rapace now enables dodeca’s dev tools to communicate with its dev server (hot module replacement for rendered markdown), and even different services on a Kubernetes cluster to talk to one another.

A screenshot of Chrome showing localhost port 4000 with the rapace docs.

Dodeca DevTools are a thing, by the way, but they're in the middle of being rewritten, so they only do live reload for now instead of inspecting the template expansion environment.

With growth and adoption across scenarios, the urge to implement other languages emerged—which demands a proper specification rather than winging it.

tracey: Bringing Traceability to Specifications

A podcast episode on traceability planted a new idea: linking specifications to implementations with unique identifiers. Every requirement in a spec gets an ID, and the code is annotated to declare which requirement it implements.

A screenshot of the Tracey source view showing some Rust code and the highlighted line has a, an annotation that Tracey recognizes.

This cross-referencing enables a two-way audit: traverse the entire spec to confirm every requirement has associated code, and traverse the codebase to ensure every piece of code traces back to a requirement. When the host noted there was no great tooling for this, the natural response was to build it.

Thus, tracey emerged—named deliberately to confuse people searching for the Tracy profiler. The tooling makes no effort to flatter the author: it shows rapace is far from spec-compliant and that tracey itself is under-specified. Support for tracey’s requirement syntax has been embedded into dodeca, so specifications can live directly on websites as clickable, referenceable entities, linking IDE tooling straight to spec requirements.

From Salsa to Picante: Making Compiler-Style Caching Async

The query system in dodeca draws heavily from salsa, the incremental computation framework used by rust-analyzer. The core idea is simple: don't recompute a query unless one of its inputs actually changed, and only evaluate queries that someone actually requested the result of.

Salsa's laziness, though, is a mixed bag. Rust-analyzer had to implement pre-warming because if you don't query anything, nothing happens — so the first completion request triggers analysis of the entire codebase. Recent salsa versions can persist caches to disk, but a huge cache makes loading costly too.

For dodeca, most operations are asynchronous — even image compression involves an async RPC over shared memory to a different cell — so salsa's synchronous query model doesn't fit. That led to picante, which isn't a fork but an async-first implementation of the same ideas.

Screenshot of the lib.rs page for salsa. It says a generic framework for on-demand incrementalized computation with a little logo that's a jar of salsa with a little dancing ferris with a maracas and a taco, I think. It says obligatory warning, very much a work in progress at this point.  Credits, this system is heavily inspired by Adapton, Glimmer and Rustc's query system. So credit goes to Eduard-Mihai Burtescu, Matthew Hammer, Yehuda Katz, and Michael Woerister.

Snacking on the shoulders of giants

salsa README

Picante makes different choices. It uses facet for everything, including structural equality comparison even when your types don't implement PartialEq. It also persists caches to disk incrementally, avoiding a big save phase at the end. While still buggy, it gives the intended result: tracking everything with little difference between production and development builds of a website.

The main difference between dodeca's two build modes is injecting a script tag for DevTools. Everything else — minification of JavaScript, HTML, and CSS, on-the-fly image compression based on browser support — happens by default. That includes something long dreamed of: codepoint-accurate font subsetting. Queries render all markdown to HTML, extract every codepoint from those pages into sets, then combine the uncompressed font with the codepoint set for each style to produce subsetted fonts. It's all lazy; the font only recalculates when you introduce a character you've never used before.

Font subsetting doesn't require shelling out to Python's pyftsubset anymore. woofwoof packages the WOFF2 C++ implementation with CI builds for Linux, Mac, and Windows. And fontcull is a Rust version of what was previously the favorite tool, glyphhanger — which hadn't been updated in five years and carried a Playwright version too old to download browsers. Font-subsetting crates for PDF existed, but they don't preserve the full font information needed for browser use. Google's fontations crates matured enough this year to subset fonts for browsers; they're vendored directly from Git and available through fontcull.

A kid raising their hand.

This is how I imagine the people in my head poking holes in my articles as I write them.

The result: facet.rs serves Iosevka at 10 kilobytes, down from a 2MB original (NerdFonts included).

Pikru: Porting Pikchr with AI Assistance

Needing diagrams for specifications and technical documentation, Mermaid was off the table — client-side rendering hurts page load, accessibility, and layout stability. D2 is written in Go. Typst was bundled briefly for Open Graph previews but became the heaviest dependency by far. Then pikchr surfaced: a self-contained C implementation, a good candidate for a Rust port.

The porting itself was delegated to Claude, given tools to compare a hundred test cases between the C and Rust renderings. A comparison HTML showed test coverage with side-by-side and onion-skin visual comparisons. The agent also got an MCP that runs a single test and renders SVGs to PNG — vision-capable models can spot misplaced lines more easily in rendered images than in SVG markup.

A screenshot of the comparison page for PIK between the C implementation and the Rust implementation.

Some of these are no joke.

Pikru

The Rust implementation uses facet-svg, types built on facet-xml, created specifically for this project. Tree-diffing algorithms produced diffs useful enough for the agent to identify rendering errors. The port, pikru, reached 100% parity with the C implementation — actual output parity, not just test coverage — with results published on GitHub Pages.

Aasvg-rs: ASCII Art to SVG

Both Claude and GPT proved poor at writing PIK diagrams, and PIK lacks auto-layout, so another diagramming solution was needed. aasvg, based on the client-side markdeep markdown implementation, fit the bill. The port, aasvg-rs, matches the original's output and uses CSS variables for light-dark support in SVG — though Safari Mobile lacks support for this, earning its reputation as the current Internet Explorer.

Facet's Expanding Ecosystem

While writing facet benchmarks, the JavaScript view kept falling out of sync with the format the benchmarks generated. The obvious solution: TypeScript types for the frontend and json-schema for validating backend output, with plain Rust types as the source of truth. Schemars exists for this, but facet promised to be the last derive macro needed. facet-typescript and facet-json-schema delivered, streamlining work on the benchmark dashboard.

Another screenshot of the facet perf dashboard showing that surjson wins at 84 nanoseconds, whereas facet tier 2 JIT is at 110 nanoseconds.

Knock knock! Who is it? Room! Room who? Room for improvement.

facet perf tracker

The ecosystem grew beyond schema generation. Alternatives to thiserror, displaydoc, and miette's diagnostic derive would be useful — but those require generating actual code: implementing Error, Display, and Diagnostic traits. That meant a plugin system for facet that reuses the parsed type definitions from facet macros while using templates to generate trait implementations.

A screenshot of the Tracey source code showing the parse error type that uses both the error drive and the Miette drive.
tracey source code

This works without another derive macro or a dependency on syn. The templates remain simple and build-time performance isn't yet measured, but the idea holds. A finite set of traits is worth implementing this way — Debug especially, which can generate substantial code for large structs even without facet in play. Facet-pretty handles introspection needs, and any trait re-implementable via reflection saves on code generation, build time, and binary size.

Virtual file systems without the kernel gymnastics

A recent project called fs-kitty demonstrates a practical use of rapace, the author's RPC library, in the domain of virtual file systems (VFS). While Linux offers FUSE for user-space file systems, macOS is a different story. Kernel extensions are risky and Apple has been phasing them out in favor of FSKit, which lets you implement a file system entirely in user space, communicating with the kernel via XPC.

The catch is that FSKit requires packaging as a file system extension, bundled as an .appex inside a regular .app. That makes it awkward for command-line tools. Some developers built FSKitBridge to add a second RPC layer over protobuf, letting you write your file system in any language and connect to the extension over TCP. That approach works but forces you to install a third-party extension, which the author wasn't comfortable with.

fs-kitty is the author's own take, using rapace for the RPC layer instead. Initially, it mixed Swift with Rust code linked via swift-bridge, but hangs and crashes led to a rewrite fully in Swift. That work inspired native racace implementations in other languages—just as you might want Svelte and TypeScript instead of Rust on the front end. The project is currently broken while dependencies are reworked, but it's a stepping stone to the author's most exciting project of 2025.

vixen: a build system with caching first

vixen is a fresh attempt at a build tool that combines buck2's rigorous caching with cargo's ergonomics. The motivation comes from measured gaps: a cold build of a monorepo takes 35 seconds with cargo but 25 seconds with buck2; a no-op build takes almost a second with cargo versus 0.06 seconds with buck2; and a single line change deep in a dependency tree costs 21 seconds under cargo but only 8.5 under buck2.

The pain point with buck2 is maintenance—hand-writing BUCK files for hundreds of crates and patching dependencies. The author's vision for vixen is a tool designed from scratch for the Rust ecosystem, that also handles C/C++ compilation, JavaScript bundling, asset manipulation, and container image builds. Key requirements include true hermeticity, a content-addressable store, and remote execution from day one.

With that design, CI can become a no-op when builds already ran locally. Artifacts live in a content-addressable store that every executor caches; there's no need to split work across YAML-defined CI jobs or stage artifacts through a separate store. Since the whole build is a single graph, the orchestrator maximizes parallelism automatically.

Hermeticity also solves subtle rebuild problems. Toolchains come from deterministic sources: Zig for C/C++ and direct downloads from static.rust-lang.org for rustc, avoiding rustup's channel paths that cause identical toolchains to trigger rebuilds. The content-addressable store makes deduplication natural—16 Rust toolchains share unchanged LLVM tools and standard library chunks, stored as streamable individual entries rather than sequential tarball extraction.

The author plans to be smarter about build scripts than buck2, which often requires manual input declarations. Rather than patching common patterns by hand, vixen would recognize them: a build script that only invokes the cc crate gets a substituted cc that dispatches actions to the orchestrator instead of compiling directly. Crates that need true special treatment, like sqlx (network access) or rustls (assembly), are edge cases a built-in package manager could handle without an external fixup repo.

With dependency tracking rigorous enough for perfect caching, you also gain deep build graph introspection for debugging why something rebuilt—a feature the author wants to expand beyond building into CI and deployment. The current implementation is only capable of trivial "hello world" builds, but the project is intended to carry into 2026.

Wrapping up a year of Rust

For the author, 2025 has been a year of intense internal development. The focus is now on dogfooding: nearly every crate in the bearcove GitHub org depends on another one from the same set. This close coupling is deliberate; it gives a personalized developer experience, and when something breaks, the responsibility is clear. There's a certain comfort in knowing a bug is your own—if it’s your code, you can fix it immediately, whereas waiting on someone else’s schedule is unpredictable.

All of this work is open source, but stability is not guaranteed—except for facet. The general rule is that if a project becomes genuinely usable, there will be an official blog announcement, likely accompanied by a video. If there hasn’t been one yet, it’s not ready.

Production and acknowledgments

This year also saw an expanded video output, made possible by working with two editors, Sekun and Vlad, who were thanked for their contribution. More videos are planned for the coming year, as they are sustainable through sponsorships.

Specific thanks were given to AWS for a substantial donation toward facet development, to Depot for providing CI build minutes, and to Zed for free credits. Companies interested in supporting these efforts can reach out via the contact information on the website’s about page.

The outlook for next year is positive—a sentiment that hasn't held true for the past decade. With a functional and self-sustaining ecosystem in place, the future looks bright, and the author’s closing note of “Take care, and I’ll see you very soon” was signed off, though a related article on image decay remains linked for those wanting more on the topic of Rust and web tooling like tide and warp.