I’ve been working on so many projects in 2025, I thought it was important for me to make a recap, if only just to clear my head.

There are many, many, many things to go through and we don’t have a sponsor today, so I’m gonna start right away with facet!

facet

facet is a project that I started working on in March of this year — that’s right, it’s only been ten months, yet it feels like an eternity.

In the beginning

I think the initial driving force for facet for me was: “I’m sick and tired of waiting for things in the Serde Cinematic Universe to compile.”

The virtue of unsynn

I’ve spent many, many moons looking for opportunities to make builds faster, I’ve written custom tooling for it…

A screenshot of my Rust article. Why is my Rust build so slow?
Why is My Rust build so slow?

I am in the top results every time I search for CI optimization, which is not a good place to be. Do not gaze into the abyss lest the abyss starts texting you at 4am asking if you’re up.

A screenshot of my article profiling linkers from January third, twenty twenty two.
Profiling linkers

Basically, the idea was that serde is highly generic: Deriving the Serialize or Deserialize trait for a Rust type already generates a bunch of code, but you don’t really pay for it yet, because it’s generic code.

It’s only when you actually call serde_json’s from_str with your type that the generic code gets instantiated and then rustc and LLVM do their best to optimize all this and that can take a long time.

A screenshot of the GitHub page for Merde that says deprecated this project is no longer maintained. Please use facet instead. Logo by Misia's Art, a simpler and slightly shittier take on serde.
github

My first attempt at tackling this, merde, had serde-like traits but mine were dyn-compatible so that you could do dynamic dispatch instead of monomorphizing everything.

Cool bear

That’s what you call instantiating generic types with concrete types, and that’s what can generate a lot of code and make builds reaaal slow—

However, reading this today I realize I just made a shittier version of erased_serde, so… that was a waste of time.

facet is my second attempt, and it’s based on the realization that it’s much nicer to implement serialization on top of reflection than the other way around.

Instead of having these visitor patterns that drive serialization in serde, you get access to an associated const called “SHAPE” for every type that implements Facet.

A screenshot of the docs page for the facet trait showing its definition.

That's it, that's the trait

Facet docs

In that shape you have information about what kind of type it is. Is it an enum? Is it a struct? You have information about which traits are implemented and how to call different methods.

Rust documentation page for the list VTable struct.
Facet docs

You also have vtables for lists, maps, and sets, along with information about fields, their offsets, and their own shape.

The first golden age

From there, things kind of snowballed. It’s like, okay, we have the information, we must have a nice API to read from existing values: I called that Peek — that gives us serialization. But also we must be able to build values from scratch using reflection.

the rust documentation page for Peek.
Facet docs

And that one’s super tricky because you’re dealing with a partially initialized object. You’re dealing with states of like… if it’s an enum, have you selected a variant yet? Have you initialized some of the fields of the variant payload? what happens to those fields if you switch to a different variant now?

a Rust documentation screenshot of the partial struct that has an associated const borrow, which defaults to true.

Borrowing is hard okay

Facet docs

It’s a minefield of undefined behavior, potential memory corruption etc. — I would never have embarked on this journey if it weren’t for miri, which catches a lot of undefined behaviour and some defined behaviour as well.

a screenshot of our GitHub Actions pipeline as of December 31st, 2025. It includes performance measurements, but also miri and valgrind.

Sanitizers are like cats: capricious and temperamenta— uhh you can never have too many.

GitHub Actions for Facet

But the good news is once you’ve written that unsafe code you can write so much stuff on top of it.

Now, deriving Facet on a struct is just a bunch of statics, and maybe a bunch of… I wanna say trampolines or adapter functions for the vtables.

And each format like facet-json, facet-yaml, facet-postcard, is just one crate that works with every type that implements Facet. There’s no generics involved. It’s all just reading the shape of types at runtime and acting accordingly.

Disappointment

Now, I knew this was gonna have a cost in terms of runtime performance, obviously, but I didn’t know exactly how much. Initial tests showed facet-json to be between five to seven times slower than serde_json.

A GitHub screenshot showing CodSpeed instrumentation performance report

That product has a very good name, but have you ever heard of flaky benchmarks? Yeahhh.

Facet PR #384

And I was okay with that, you know, it’s still the same order of magnitude. I was still hopeful that it would actually be faster to compile. You would gain in build times, you would gain in binary size, etcetera.

And then I measured build times and binary sizes, etcetera.

And it turns out that not only was it slower at runtime, it was also slower to compile and bigger in binary sizes.

A screenshot of my blog article about the initial introduction of facet.
Introducing Facet: reflection for Rust

So when I made the video of the announcement of facet, I was like, well, things are not exactly what I would like them to be right now, but I have to say the truth.

Like, you have to share the numbers, right? Otherwise what are we doing here? Knowing the situation is the first step towards improving it.

A screenshot of the introducing facet reflection for Rust YouTube video that shows numbers and it shows facet being mostly slower, except for serializing long strings up to six times slower.

I'm not owned, I'm not owned, I repeat, as I go deeper and deeper into my side project that has come to consume my entire life.

YouTube — Introducing Facet: Reflection for Rust

But that kinda ate into my enthusiasm and for a while there I was just focused on other projects I think.

Refocus

And in October of 2025, someone started porting their codebase, big proprietary codebase, from serde to facet. And they encountered a million bugs, which I told them to report individually on the issue tracker. And so we had I think a couple weeks of back and forth, like, oh here are these four new issues and me fixing them as fast as I could.

And one of the issues was build times. Build times got worse switching over to facet. Part of the reason is that facet generates a lot of code. And part of the reason is that it’s really hard to completely switch away from serde and syn and other crates like that because they are so prevalent. So you might still be paying for them somewhere else.

Maybe tracing pulls them in. Maybe you have a derive macro somewhere. Maybe some crate uses serde_json internally just to have a value type. So now facet, which isn’t free, is on top of the ecosystem you were trying to move away from.

And it’s during those two weeks that I decided, you know what, we’re not gonna try to be the smallest, fastest to compile, fastest at runtime crate. We’re just gonna try to be the nicest, in terms of developer experience (DX).

A post of mine on Bluesky saying, incidentally, I think I'm kind of giving up on the idea of facets being smaller than alternatives and committing to the idea that facet is nicer slash more featureful than alternatives. It has twenty-nine likes and one save, it's from November 25, 2025.
bluesky post

I started focusing on adding features that would be nice to have, like best of class error reporting for parsing errors using miette, streaming deserializing from AsyncRead, etc.

I started working on facet-solver, which gives you the best error messages for untagged enums and flattened structs and everything, things that are really hard to resolve. You need a complete view of the shape of the types you’re trying to deserialize and you need to know everything that happened up to this point.

screenshot of the facet KDL showcase showing that the solver couldn't find any matching configurations. It shows all the variants it tried to match, it says the fields that were unknown, it shows that it's trying to find fields that have similar spelling

There's a bit of redundancy here that happened when we improved the display implementation. I'll fix it next year.

facet KDL showcase

And that’s exactly what facet-solver does, and it does it for all the format crates! Not just for JSON!

Because we have so many format crates, we have JSON, YAML, TOML, Postcard, MsgPack, XML, KDL, but also now SVG, HTML, CSV, XDR, query strings, command line arguments, ASN.1 — features tended to drift between crates. Like you implement something about untagged enums in JSON and suddenly YAML doesn’t support it, so you have to fix all crates individually.

A screenshot of the format creates comparison page with a table listing all the different packages and whether we can serialize or deserialize using them, etc. etc.

You think it's fun maintaining all this? Well it... kind of is actually.

facet — Format crate matrix

To combat this, I introduced facet-format, which is the successor of facet-serialize and facet-deserialize, and it’s the basis that all format crates are today based on. Yes, that meant rewriting all the format crates and then renaming the old crates to legacy and the new crates back to the old names and then deleting a hundred thousand lines of code in one PR.

A screenshot of a GitHub pull request saying remove all legacy format crates number 1537. It's merged 14 hours ago. And the diff is plus 20 minus 103083. The common says we've done renaming and now it removes all the legacy crates.
facet PR #1537

It was glorious.

(And there’s probably a hundred regressions I haven’t found yet, despite careful planning and execution.)

Volte-face

And in the middle of like adding a lot of features to facet to make it the nicest thing ever, I decided you know what? No, it was supposed to be lighter. It’s bullshit that it generates more code than serde and that it’s actually slower to compile. I don’t like that.

So I started working on reducing the amount of code generated: There’s a handful of tools you can use to do that. One of them is cargo-llvm-lines, but you can also use the “-Zmacro-stats” unstable rustc flag, and of course you can do rustc self-profiling.

And using those I was able to get to a point where a bloat benchmark using generated struct and enum types was actually faster to compile with facet than with serde.

A GitHub comment from VRON saying that's a significant win from before by replacing Circuit with facet, the rest is cutoff, and then it says release mode ninety-eight seconds to eighty-three seconds, debug mode fifty-three seconds to forty seconds.

That was a couple months ago, but I checked just before writing this, and apart from a couple dependencies that slipped in by mistake, things are pretty even still.

Your browser does not support the video tag.

More importantly, there is now tooling to compare facet against its past self in terms of lines of LLVM intermediate representation generated, compile times, compile size, etcetera. I made a little text user interface using ratatui to look at all the records which are tracked in the repository :)

The measurements are done on a synthetic code base, but I’ve gotten reports that it does translate to similar results on real-world codebases. It is a moving target though and things evolve as we add more features.

Just-in-time for the new year

Another thing I said in my “trying to be optimistic” announcement of facet initially was like, well, okay, it’s.. it’s slower at runtime, but maybe we can do JIT, just in time compilation, and then it might even be faster!

And for a long time I kept using that as a kind of shield of like, yeah, it’s slow, but that’s because we haven’t tried doing the real thing yet. And I felt like it was dishonest and I was tired of using it as an excuse and I decided to just make it happen, with cranelift.

A screenshot of the CraneLift code gen page on lib.rs saying it's a low-level code generator library. This crate contains the core CraneLift code generator. It translates code from an intermediate representation into executable machine code.

Well, when you say it like that, it doesn't sound too complicated.

cranelift-codegen lib.rs page

And at first I made what I’m calling tier one JIT that all formats can benefit from. It’s: instead of assigning fields etc. via reflection, you just generate code that does it directly for you.

And you get some decent performance benefits just from doing that, but it gets even better if you go to tier-two JIT, which is the format crate, like facet-json, knows how to parse JSON, so you just emit instructions on how to parse JSON and on how to assign fields like construct those types from the source material at the same time, in the same code.

And then that gives you a lot more performance. There are caveats of course, We don’t get the auto-vectorization that LLVM can do. We don’t get to inline calls into the standard library, so we have to be smart and do things like use staging buffers and then do Vec::from_raw_parts, from hash maps you can build a slice of key/value pairs and then build the map in one shot with from_iter.

There’s a ton of little tricks that went into this, but as of today I’m happy to report that if you’re okay with somehow depending on cranelift at runtime, with having code generated that is nigh-impossible to debug, might contain undefined behavior and crash your program and whatnot, you can now beat serde while staying in the facet ecosystem, at least for JSON.

A screenshot of perf.facet.rs showing diverging bars for facet.json tier two JIT versus serde_json
facet perf tracker

I know, because I made a performance dashboard that tracks facet-json versus serde_json using divan for benchmarking time and gungraun for benchmarking instructions.

A screenshot of the benchmark for the postcard crate.

I would not necessarily trust these results. I think we need to verify them. 0.03x seems suspiciously good — there might be some zero-copy apples-to-oranges shenanigans going on there.

facet perf tracker

It’s not on there, but I also enabled it for facet-postcard just for fun, and it is indeed faster than the reference postcard implementation. That said, should you use it, like I said, there’s a bunch of caveats.

For me, I don’t mind. I think it’s funny. I think most programs would be like, we can take the performance hit of reflection, or like, we’ll just stay using serde, or a thing I haven’t explored yet, but I know would work is doing codegen from facet information.

You would depend on a “types” crate as a build dependency and generate serialization and deserialization code from a build script, much like the derived macros of serde does. That’s an option I’ve only really explored for one specific use case that I’m getting to.

But enough about facet.

arborium

In the meantime, I’m working on lots of Rust crates. I’m working on lots of different formats. I notice in my Rust docs that blocks of KDL or TOML or whatever are not highlighted on docs.rs, and that makes me very sad.

Meanwhile, I’m also working on some private proprietary projects that I’m not going to talk about, and I need to do syntax highlighting using tree-sitter, which is great, but I always have to go chase grammars that work, and I always run into problems compiling them to Wasm with a Rust toolchain.

There’s a bunch of linker hacks, I’m trying out Buck2, which doesn’t help things at all. It’s a nightmare.

I decided to start working on the definitive Rust distribution of tree-sitter and tree-sitter grammars called arborium. So I go and find 96 grammars, I figure out a good API, I make sure that they all have syntax highlighting queries, I bundle a bunch of themes, I make a nice little landing page for it.

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

I make sure they’re able to compile to WebAssembly by faking all the C functions they claim they need. And I spend forever automating CI so that I can actually release updates to the grammars and the themes and the crates. I make sure that license information and attribution is still there.

I get in touch with the crates.io team saying, I’m sorry, I’m going to be publishing 100 crates tomorrow. Is that okay? And they’re like, yeah, you’re already on the list of people who can do that stuff which means, somehow you’ve done weird things before.

And tada, arborium. I’m super, super happy about this release. It’s already useful to a bunch of people. I hope it becomes useful to a bunch more people in the future. But for now, it’s just so satisfying to comprehensively solve a problem and just never have to think about it again.

I say that as knowing that still, in the back of my brain somewhere, I’m like wouldn’t it be nice to have a pure Rust version of tree-sitter instead of having generated parsers in C and a C core and everything?

A screenshot of the GitHub comment about whether it might make sense to have a pure Rust tree sitter implementation. It says:   A screenshot of the GitHub comment about whether it might make sense to have a pure Rust tree sitter implementation. It says:
Arborium, issue #55

It might, it would also be slower to compile. It would also be like a lot of work. It might be reasonable if you’re willing to give up some performance and give up all the incrementality of tree-sitter, then maybe. But I don’t need that, so I’m not doing it.

dodeca

Meanwhile, I’m working on facet. I decide we need a proper website with proper documentation on there. What are the options for a static website? Zola. Everybody loves Zola. It’s Rust, it’s our Émile Zola to their Victor Hugo.

Well, I have very very strong opinions when it comes to making websites, both the experience of making the website and what the results should look like.

So you become aggravated with little things, little paper cuts, little developer experience problems. And the fact that it’s harder than it should be to just make a plugin, right? I’m just looking around for an SSG that will just let me make plugins. And in Rust, that does not exist.

In other languages, sure. In JavaScript just fucking eval it straight into my veins. In Ruby, require it, in Python, good luck with all those paths. But in Rust, nope! Largely, nope.

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

And I’m reminded that if I somehow forked it and added everything I wanted, I’m reminded that even if I did all of that, then it still would be pretty average at caching.

Just like pretty much every static site generator, it would cache things that are stale, and it would fail to cache things that it should be (by using conservative HTTP headers). And that just makes me aggravated. So I decided to make my own, named dodeca after the dodecahedron, a nice shape.

And you know, how hard can it be? It’s “just” turning Markdown into HTML.

A picture of an Asian woman shrugging and looking mischievous slightly.

That's the attitude that got me through the whole year.

That part’s easy, pulldown-cmark, want syntax highlighting? I just made arborium. Super. I want minification for HTML, JavaScript and CSS built-in, there are crates for all that. I want cache-busting, of course of course — now I need to rewrite HTML so link, script and img tags point to cache-busted URLs.

I want image processing built-in: PNGs go in, JPEG-XL, AVIF and WebP are served to browsers — you better believe we’ve got either pure Rust implementations or wrappers for the original C/C++ implementations.

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

And at some point I’m 1200 dependencies in…

And iterating becomes really painful.

I’m reminded of the time I made rubicon to enable dynamic linking even if you’re using crates with thread-local statics like tokio, tracing, etc.

A screenshot the GitHub page for Rubicon

I'm still really fond of this logo by Misia.

GitHub — rubicon

But I don’t want to have anything to do with dynamic linking anymore.

So what’s the next best thing? IPC.

rapace

IPC is just RPC at home, so I named my thing rapace, which is RPC with extra letters, and is French for “bird of prey”.

And I have again, strong opinions about how to do RPC. I’ve been doing it for a while. There are things that I like and things that I don’t like. For example, gRPC, I don’t like. Mostly because of protobufs, which have all the downsides of Go with none of the charm.

And I figured I have all these different patterns that I want. First off, to make iteration easier, I’m going to have the central app, the hub, be its own binary. And every cell around it be its own binary. And then you put like HTML modification in one cell.

You put image compression in one cell. You put even HTTP serving in one cell. Like text user interface in one cell. Everything is a cell. I have 18 cells right now in dodeca.

And yeah, I want to do this over shared memory because I’m giving up on dynamic linking, but I’m not giving up on performance, you know. If you have to compress a large image, it’s nice if you don’t actually have to make several copies of it over the RPC system.

So to do this right, you have to set up your shared memory as kind of an allocator buffer pool. You have to keep track of which buffer is owned by whom.

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.

When sending the uncompressed image payload, like, all the pixels, between two IPC peers, you can do that in a zero-copy fashion if you treat it as a reference or a handle to the allocated memory (and if you used a special memory allocator, which takes from the shared memory area).

I’m not exactly there yet, but I have eliminated quite a few copies and I’m doing zero copy deserialization, where you get the frame back and then you deserialize borrowing from the frame and then you just carry the frame and the deserialized payload together, like you would do with the yoke crate, but without using the yoke crate, because it relies on syn!

Obviously, rapace uses facet-postcard for serialization and deserialization and that makes it super easy to discover services dynamically and use them, call their endpoints without even knowing about them at compile time. So you can make dashboards that explore services, and there is one in the examples of rapace.

So I did this whole complicated shared memory design. But, you know, once you have RPC semantics, it’s tempting to try other transports. For example, why shouldn’t I use that to have the dodeca dev tools talk to the dodeca dev server to get things like, you know, hot module replacement, except instead of modules, it’s paragraphs of markdown getting rendered to HTML.

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.

Why shouldn’t I use the same thing for different services on my Kubernetes cluster to talk to each other? And so next thing you know, you have a shared memory transport, of course, but also a WebSocket transport, a generic stream transport, an in-memory transport for testing, of course.

Again, there was kind of a rapid growth era where I just added rapace to everything I could think of, and it worked more or less, and then I started thinking about implementations for other languages and I figured okay I need a proper specification. Enough with just winging it.

tracey

And that reminded me of something that James, my podcast co-host on self-directed research, taught me about this year, which is traceability. You want to have a specification and you want to have an implementation and you want to have links between the two. You want to have like every requirement in the specification has a unique identifier.

A screenshot of the Tracy web UI showing the Tracy specification itself with a bunch of rules shown in the spec, like a rule reference must be enclosed in square brackets within a RAS comment. And it's in a little section with the rule identifier and also where it's implemented and where it's tested.

And then you annotate your code to say this implements that requirement.

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

And then you cross-reference it. You can go through the entire spec and see if everything is implemented in the code.

A screenshot of Tracy's coverage view, which is a table. There's rule on the left and then references on the right. Everything is clickable, of course. It also shows implementation coverage and test coverage at 100%.

And you can do the reverse too! You can go through the code and see if all the code is covered by requirements.

there are screenshots of the Tracy sources tab at this time. It's, there's no, it's not in the context of a rule. So it's just showing a file tree and how much of each file is covered by a spec rule.

And you know what, this is very much in the spirit of my year 2025. But I remember James saying there’s no great tooling for it. And I didn’t even check. I just went and made my own immediately, which I called tracey, mostly so that people who are looking for the Tracy profiler get confused and use my software instead.

And you know, now I have a great interface to let me know that ther apace implementation is very far from being spec compliant. And also to tell me that the tracey application itself is very far from being fully specified.

I have added support for the tracey requirement syntax to dodeca so that you can embed the specification on your website and just have it be clickable and refer to it, which means you can have IDE tooling that refer to a spec requirement and that links directly to the website.

picante

Speaking of dodeca, another very important part of it is the query system. I’ve been obsessed with salsa ever since I’ve learned about it — basically, when you write something like a compiler, you have a bunch of inputs and you have a bunch of queries which can read from those inputs or from other queries, which themselves can… you get the idea.

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

And the goal is super simple: be as lazy as possible. Don’t recompute a query unless anything that goes into it, one of the inputs to it, has actually changed. And of course, only evaluate the queries that you actually need, that someone actually requested the result of.

salsa is used in rust-analyzer and the laziness is a blessing and a curse. They had to implement pre-warming because if you don’t query anything, it’s not doing anything. So the first time you ask for a completion, it’s like, whoa, buddy, I have to analyze this entire code base for the first time ever.

As of fairly recently, salsa is able to save a cache to disk, which can be, again, a blessing and a curse, because what if the cache is huge and now loading it from disk is extremely costly as well.

Because I wanted perfect caching in dodeca, I started using salsa! But I fairly quickly ran into the problem that most of my operations are asynchronous. Even just compressing an image is making an async RPC over shared memory to a different cell. Therefore, salsa doesn’t work for me because all the queries are supposed to be synchronous.

A screenshot of the Picante homepage that says async incremental queries, salsa style memoization and dependency tracking built for Tokyo from the ground up.
picante homepage

And that is how I started working on picante, which is not a fork or anything. It’s just like the same ideas as salsa, but async first.

I had to make a bunch of different choices there. It uses facet for everything, of course, including equality comparison, even if your types don’t implement PartialEq, it just does structural equality, which is nice.

I did also implement persisting the cache to disk and even incrementally persisting the cache to disk, so you don’t have a big save phase at the end.

I’m sure there’s still a lot of bugs in picante, but overall it’s been giving me what I wanted: track absolutely everything. In dodeca, there’s very little difference between the production build of a website and the development build of a website. The main difference is that we inject a script tag for DevTools.

But we do minification of JavaScript, HTML, and CSS by default. We do, of course, image compression on the fly, depending on what you request, which is to say, depending on what your browser supports…

…and we do something that I’ve always dreamed of doing: codepoint-accurate font subsetting, which requires parsing styles and interpreting them and collecting Unicode sets, codepoint sets from all the pages on your website.

So there’s a query for like rendering all the markdown to HTML. There’s a query for extracting all the code points from HTML and putting them into one set. There’s a query for taking the uncompressed font and the set of all the code points for a certain style together and doing the font subsetting.

And it’s all lazy. Like it only recalculates when you use a character you’ve never used before. Like suddenly you paste in something that has Unicode box drawing characters and yeah it needs to add them from the original font.

A kid raising their hand.

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

But Amos, isn’t font-subsetting expensive? You need to shell out to Python to use pyftsubset. No, it’s not, because I released woofwoof, which is just a build of the WOFF2 C++ implementation. But I packaged it nicely. I made sure that it builds in CI for Linux, Mac and Windows.

And I released fontcull, which is a Rust version of what was my favorite tool for that up until that point called… glyphhanger. The problem is that glyphhanger hadn’t been updated in five years and was using a very, very old version of Playwright, old enough that it couldn’t download browsers anymore. And I just decided, fuck it, let’s do it all in Rust. We have the crates.

Or do we? Because I’ve been keeping an eye on this for a while, and for font-subsetting, there was only something that worked for PDF, which doesn’t need the full font information. So it couldn’t be used to subset fonts and then use them in browsers. But I also knew that some people at Google were working on a bunch of Rust crates around fonts called fontations.

And the good news is that this year they came close enough that you can use their crates to subset fonts and use them in browsers. And I know that because I vendored their code straight from Git, they’re not released on crates.io yet, except as part of fontcull.

A screenshot of the dev tools for a facet.rx page showing that it is only 11 kilobytes.

And the result is that if you go on facet.rs, which does use dodeca for the docs, and you look in the network tab and you filter by fonts, you will see that the Iosevka font being served is 10 kilobytes down from the original 2MB (which includes NerdFonts etc.)

pikru

Speaking of dodeca, since I knew I was going to use it for specifications and technical documentation, I wanted to have a way to make diagrams, but I didn’t want to use Mermaid because I don’t like client-side rendering. It’s bad for page load time, it’s bad for accessibility, it’s bad for page shift.

I just hate it.

I looked around for alternatives, I found D2 which is made in Go, but it’s made in Go. I found Typst which I bundled for a while to make Open Graph previews for pages automatically, but dear lord it was my heaviest dependency by far.

Eventually someone pointed me to pikchr, a diagramming solution that I didn’t know at all, and that had a completely self-contained C implementation, a very good candidate for a Rust port. The thing is I didn’t feel like porting it myself. So I essentially set off Claude to port it by giving it the tools to compare a hundred test cases, the rendering between the C implementation and the Rust implementation.

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

Some of these are no joke.

Pikru

I had to generate a comparison HTML to show test coverage, and for each test, a visual comparison side-by-side, onion skin, etc. That’s for me, that’s for humans. And then for it, I made it make for itself an MCP that runs a single test and then renders two SVGs to PNG because those models have vision capabilities.

So sometimes it’s able to look at the thing and go, I see that the lines are in the wrong place. Whereas if it were to compare SVG, it would just be drowned in all the markup. Speaking of comparing SVG, the Rust implementation also uses facet-svg, which is just a bunch of types defined on top of facet-xml, which I made just for this.

And I worked on a bunch of tree diffing algorithms just to produce diffs good enough so that the agent was able to tell, oh, this is what’s wrong with the rendering.

Eventually the Rust port reached 100% parity with the C implementation — not test coverage, actual output parity — all the tests passed. I published the comparison HTML on GitHub Pages and named my implementation pikru.

aasvg-rs

It’s at that point that I discovered that actually Claude and GPT both suck at writing PIK diagrams. So unless I make the diagrams myself, which I don’t really want to (there’s not a lot of auto layout going on in PIK), I decided to port another diagramming solution.

I didn’t know that something like svgbob existed (and already has a Rust crate…). Instead, my research found aasvg, which is based on a client-side markdown implementation called markdeep.

An ASCII art diagram.

Before...

The same ASCII art diagram rendered with aasvg, with beautiful lines.

And after!

My port is called aasvg-rs, unimaginatively, And it also has parity with the original and it’s doing that nice thing that I did in both my ports where it’s using CSS variables to get light-dark support in SVG.

Cool bear

Little caveat, this is absolutely not supported by Safari Mobile, which is the current Internet Explorer.

facet keeps growing

More things have happened in the facet ecosystem that I haven’t really talked about. As I was writing this, I worked on the facet benchmarks, and I was like, oh, the JavaScript code for the benchmark browser view keeps getting out of sync with the format generated by the benchmarks.

If only we could use TypeScript types to validate the frontend, and if only we could use json-schema to make sure that the backend is producing what we think it is, and if the source of truth was just a bunch of Rust types, Wouldn’t that be fantastic?

Well, obviously, there’s existing solutions, existing derived macros like schemars, But at some point, I promise that Facet would be the last derive macro you’d need. And in this case, it works! I quickly threw together facet-typescript and facet-json-schema, which made iterating on the benchmark dashboard a lot easier.

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

Other things I was interested in included an alternative to thiserror, but based on facet, or something like displaydoc or derive for the miette crate, so you can implement diagnostic without either doing a manual implementation or using something that depends, again, on syn.

And the problem with all these is that you actually have to act like a macro. You have to generate additional code. It’s not just, oh, you derived Facet, so you can do whatever you want at runtime. You have to implement Error.

You have to implement Display. You have to implement Diagnostic. Therefore, you have to generate code. Therefore, we needed to come up with some sort of plug-in system for facet. something that would reuse the result of parsing your type definitions that facet macros already does, but that is able to use templates to generate different implementations. And we did exactly just that.

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

It’s not entirely final and the templates are pretty simple for now, but it works. Like, you don’t need another derive macro, you don’t need syn. I have no idea what the performance is like. I haven’t actually measured build times on any of this, but the idea is pretty simple.

We shouldn’t need too many of these because there’s a finite amount of traits that you really want to implement: I barely bother implementing Debug anymore, because if I want to see what’s inside a type, I just use facet-pretty: Every trait where performance is not paramount and that can be re-implemented just using reflection is savings in terms of code gen, build times, final binary size, etc.

The Debug trait especially; even if you’re not planning on using facet, look at how much code debug is generating for large structs etc. It can make up a solid chunk of your binary.

fs-kitty

One very cool application of rapace that I made recently is fs-kitty which has to do with virtual file systems.

The SSD that’s on your laptop, has a real file system on it. Maybe it’s ext4, maybe it’s btrfs, maybe it’s ZFS if you’re the good kind of nerd. Or, you know, APFS or NTFS, whatever.

Sometimes you want to access files over the network and then you would use something like Samba or NFS. And sometimes you just want to kind of make up files like pretend you mounted a zip file for example. And for that you need a VFS, a virtual file system.

On Linux, if you want to make a virtual file system, you can use FUSE, which means file system in user space. And on Mac, you could make kernel extensions but of course anything that runs in the kernel must never crash, otherwise everything crashes because monolithic kernels won the war.

Therefore, Apple has been trying to kill kernel extensions for as long as they’ve been a thing, and they’ve introduced things piecemeal to kind of let companies like Dropbox have their virtual file system without touching the kernel as much.

The last piece being FSKit, which lets you implement the file system entirely in user space, communicating to the kernel over XPC, another form of RPC. Which is great, except you have to package it up as a file system extension, as an .appex bundle, which registers in system settings when you open the associated regular .app bundle: It’s not really designed for command line tools.

But I saw some people made something called FSKitBridge, which adds another layer of RPC (they used protobuf). That way you can implement your file system in any language. And their file system extension connects to your binary over TCP. And, you know, it’s a little layer cake of RPC that works.

And you don’t have to worry about the terrible Apple requirements. You don’t have to ship an app yourself or sign it. But it’s an extension that, I don’t know, I just didn’t trust these guys to install a file system extension from them, even though it’s fully in userspace. I don’t know, it felt wrong.

a screenshot of the FSKitty application. The app logo is a round kitten sleeping.

So I made my own, fs-kitty, but using rapace for RPC.

So all the app itself and most of the app extension is in Swift. And initially, I just compiled a bit of Rust code and linked it, called it from Swift using swift-bridge, which does support async.

And eventually I thought, you know, I’m getting hangs, I’m getting crashes. Wouldn’t it be easier to just implement everything in Swift? That’s when I started writing rapace implementations in other languages, just like, you know, Rust on the front end is fine. But sometimes I just want to do Svelte and TypeScript and be done with it. Wouldn’t it be nice to just have like a native TypeScript implementation of the rapace protocol?

As I’m writing this, it’s at a stage where… it used to work at some point, and then I started reworking all the dependencies. Therefore, it’s broken right now, but it’s going to work again in the future, let me tell you.

Because I need it for th