arlewatyerre tuhoro-bell
11 jun 2026writing

how I spent a week rethinking a thesaurus

written in narrm after a long day on the phone to suicide counsellors

I used to be an Apple user. Shock, horror. But corporate life offers you two pathways with technology, and both routes are like bricks (in)explicably rope tied to your ankles as you sink in the ocean to the depths of Microsoft’s 365 suite of tools. Some #indie #cool workplaces use tools like Google docs or Slack – same circus, different monkeys. There is an intentional tethering of all modern semi-corporate computing to multi-billion dollar behemoths, from Microsoft to Amazon, Google to Apple, Samsung to Oracle. Big tech has a noose around the corporate sphere’s neck, and it is an overwhelmingly American noose. This has triggered some sovereign digital technology movements in Europe. But digital sovereignty in the European sphere is a much different beast to what those of us moving in anti-capitalist and Indigenous circles are thinking. It’s more of a “we (can) have big tech at home”.

Australia’s government, just in the last few days, has been mulling over increasing taxation on data centers, particularly those with big power bills, but this harms any notion of digital sovereignty more than it helps. It will force smaller Australian providers out of the market, while big (American) corporate players like Amazon, Akamai, Cloudflare justify increased edge prices for Australian corporations, so it effectively adds additional operating costs to Australian businesses by proxy for more American proceeds. Hmm.

But, Aidan, you said this was about a thesaurus, what the fuck?

Okay, so sovereign to me means that we have agency in the ways, the tools, and the relationships we foster and develop in our lives. Digital sovereignty, then, means not letting (American) megacorps inventory, catalogue, probe, consume and analyse all facets about us at the most minute, intimate and detailed level of our lives. Just like how Facebook tries to sell you underpants in-feed just after you mention your missing sock. It’s all a game of surveillance and sales. Make money go big. Line go up. disgruntled stock exchange monkey noises. Can you tell I have a high opinion of day traders?

I’m currently wading through a lot of personal shit, so I might be a bit scattered. Don’t worry, this is art house cinema.

So why a thesaurus?

I have long been a Unix person; my grandfather raised me around some pretty arcane, and at the time already archaic Unix System-V boxes. We were a Sun Microsystems household which couldn’t afford any of the microsystems. The cruel irony is the founding technologies there which were oft open-sourced have become Oracle’s sticky little tentacle over human resources worldwide. The Mac was a welcome relief from Windows Vista and I didn’t look back.

As with many, on the server side of things I have developed an above average command line proficiency. I know my way around UNIX-Like systems, and Debian is a lovely and stable server. And its actually free software. Beautiful.

Its no coincidence that the EU has started to migrate many of its government systems away from Oracle and Microsoft towards their own (sometimes even open source!) systems built on Linux1. Linux is a free and open source kernel, and there a bunch of free and open source tools that superglue on to it to make it do computery things. Naturally, the “big boys” don’t like that on the client side, you could get for free what they want you to pay for (in license fees, ad revenue and constant surveillance – microsoft, or in exorbitant device costs, ad revenue and “privacy”2 – apple). However, all the big-tech bros use varying forms of Linux (I’m not explaining the nuance of this today) on their servers. Despite their large benefits from the free engineering Linux provides, they still spend the rest of the time telling us-all not to use free or open source software on the client side, because it makes them bulk cash dollar.

Could you please explain the god damn thesaurus? I hear you yell at your screen – well its on you for reading a post titled toward a thesaurus, suckerrrr. Anyway, all this…

means there’s quality investment in the Linux world for servers, and I was a believer that Linux on the desktop was an uphill battle not worth it. But actually, it’s remarkably easy, and because it requires a little more thought than just getting a shitty HP laptop for $300, it means you’re more intentional about your tools and how you use them. Yay. It also means I get a little closer to digital sovereignty, because the stack I run is all free and open source, which means it is audit-able. I can see where the data goes, if it goes anywhere, but most creators in the Linux world are already privacy conscious anyway.

But the Dictionary on MacOS has been a massive lifesaver for me ever since I did my PhD. In the past, when I last tried a full time transition to Linux, I tried to create a dictionary app with GTK+Python, and it didn’t go well. It sort of worked, proved the concept I suppose, but it was filling big shoes.

Enter wordnet. Scholars and computer humans who are collected under a banner of “Princeton University” did a bunch of work on defining and mapping relationships between words across the 2000s. They released this work for free and open source purposes. Apps like Artha use WordNet to create a dictionary/thesaurus on steroids; not just synonyms and antonyms, but word categories, applications and derivations of words, it just blows Apple’s Dictionary out of the water on every front. However, Artha is OLD. Wordnet is OLDER. and to make matters worse, Wordnet, like many public goods that university staff start are eventually abandoned for the cash-in-hand projects.

This means that, between a rather large database of thousands of words and relationships, and getting a semi-functional dictionary going in the year 2026 can be … patchy. And there are current maintainers doing that gruelling work to make sure Wordnet stays up to date and Artha keeps running as an app. But both of these are on borrowed time and neither are maintained by their original creators any longer.

There’s three layers: data, interpretation, and display.

Because I want to plug holes in my tools where I would’ve reached for something proprietary in the past, I set out to make a display for wordnet that uses newer, more maintainable code types (though still C) and then, because I’d not done it before, made an Android app that does the same. But both depended on that old, partially enigmatic, a little unreliable code. And the Android port needed a lot more work to get the interpretation engine working because while Android is Linux, the interface is not and uses Kotlin rather than C.

Rot under the floorboards

In the initial releases, all I did was rewrite the display layer. This meant the other two parts were still languishing. The desktop app borrowed its engine from Artha directly: a file called wni.c, 2,052 lines of C that I treated like a dinosaur fossil in a museum, which is to say I poked at it despite the no touching sign. But it was quarantined, it isn’t mine, it’s GPL-2 under someone else’s name, and it worked, which in legacy C is a state you disturb at your peril. And wni.c itself sits on the WordNet C library, which Princeton abandoned the better part of two decades ago.

So how does an abandoned C library still build in 2026? Debian. Specifically, a stack of twenty-two patches that Debian’s maintainers cart around so the thing keeps compiling3. I vendored that stack into my repo, and the directory listing could almost be tree rings (no it couldn’t shut up). A selection:

01_tcltk8.6.patch                          6,257 lines
50_CVE-2008-2149_buffer_overflows.patch
60_gcc-4.6.patch
gcc-14.patch
gcc-15.patch

A security hole old enough to vote. A patch to humour GCC 4.6, then another for GCC 14, then another for GCC 15. Every time the compiler gets stricter (rightly so), somebody has to apologise to it on WordNet’s behalf. The code stands perfectly still while the world moves on (corporate or otherwise), and the patch stack piles up ad infinitum.

The Android port was worse in a different way. I had re-written the lookup rules themselves in Kotlin (because how tf do you run C on Android, please tell me – no, really, please tell me?), and proven them byte-for-byte against the desktop app. Gruelling, and actually deathly boring. But the reading of the dictionary files went through extJWNL, a Java library (like why did google go “oh I know, let’s use Java” – I blame my childhood Sun Workstation usage for my Java resentment) which insists on opening the dictionary read-write. Android’s sandbox, quite reasonably, hates that. So I was copying the database around and planting fake empty files to soothe a library built for a chunkier, more trusting filesystem. Big. Yikes.

The running tally, for folks at home who haven’t walked away in despair for the depth of this nerdy rabbit hole: one set of rules, three implementations, two of them other people’s abandonware.

One engine to rule them all

But they were, all of them, deceived, for another Ring was made. In the land of Mordor, in the fires of Mount Doom, the Dark Lord Sauron forged in secret a master Ring… god I’m loopy.

a fix? write the interpretation layer once, rule it, and let both apps share it. and thus my waters broke and into this world popped Samwise Gamgee. No, wait, Hairless ‘Smooth Lightening’ Potter?

So anyway I made onym-engine, a Rust core that replaces wni.c, the WordNet C library, and extJWNL in one go. you can sigh a sigh of relief with me now (that’s your queue ahhh).

Here’s the thing that makes this safe, and a more fun challenge. WordNet’s data is effectively finished. It is unlikely to update again. That sounds like decay (like it’s not going to have newfangled words, sadly), but for an engine it’s a gift – and what do the say about teeth and horses and trojans? of course it’s that you can write the thing once, prove it, and stop (HA!). No moving target.

The discipline went like this:

  • Specification first. The behaviour contract is a document with al the expected behaviour, including some genuinely weird ones, each kept on purpose.
  • A conformance kit: 821 golden (fucking) fixtures capturing exact byte-for-byte output for hundreds of words … so many words … completions and misspellings.
  • Transcribe, don’t improve. The old engine’s quirks are behaviour real users of this have grown accustomed to, like a well patched jacket worn daily for more than fifteen years (might be time to move on: but you know you never will… and the patches make the goodbyes harder still). You keep them and write them down; you do not “fix” them by accident and call it progress.
  • Exactly two deliberate fixes, both genuine iteration-state bugs, both specified before any code changed. One truncated variant lookups, which was hiding pica-pica’s entire magpie homograph4. The other attached tree branches to the wrong parent, so “door” had grown a phantom child. I wonder if I’ll ever grow a phantom child. Actually, are they in the room with us right now? Dr Bell, are you okay? Wake up, sir?
  • Then the real proof: dump (no, not like shooting a hoop) every one of the 145,420 headwords through the proven old engine and the new Rust one, and compare bytes. cmp returns silence. (That total sweep caught two behaviours the 821 fixtures never exercised. The corpus is not the oracle. The full diff is.)

The swap landed in the GTK app as 53 files changed: 415 insertions, 11,546 deletions. The patch directory is gone. Nobody will ever have to write gcc-19.patch for this thing. Yas quane.

I may need a tetanus injection, the Rust is real

I’m not trying to be fashionable. Create exhibits. Here is the old world, real code from wni.c, the heart of how Artha answers you:

GSList *global_list = NULL;

static gpointer *get_from_global_list(WNIRequestFlags flag)
{
	GSList *temp_global_list = global_list;
	WNIRequestFlags temp_id = 0;

	while(temp_global_list)
	{
		temp_id = ((WNINym*) temp_global_list->data)->id;
		if(flag & temp_id)
			return ((WNINym*) temp_global_list->data)->data;

		temp_global_list = g_slist_next(temp_global_list);
	}

Every lookup walks a global linked list. The whole engine is one shared mutable blob (sometimes I wonder if I’m an immutable blob); it works because one window asks it exactly one question at a time, and everyone involved promises they won’t sneeze.

But here comes spring.

Then there’s now, the new front door:

//! This crate owns the model, the morphology, the lemma index, and the lookup rules. It reads
//! the WordNet database files from a directory the caller supplies and holds no global state.

#![forbid(unsafe_code)]
    /// Open an engine over the WordNet 3.0 database in `data_dir`. The directory is read in
    /// place, read-only; no environment variables are consulted.
    pub fn open(data_dir: impl AsRef<Path>) -> Result<Engine, OpenError> {

#![forbid(unsafe_code)] is not a code-review aspiration; the compiler enforces it. This crate (don’t worry, its a rust thing and even I don’t really know what it is) cannot contain the class of bug that 50_CVE-2008-2149_buffer_overflows.patch exists to apologise for. No global state, so an engine handle is safe to share. An explicit directory, read in place, read-only, no environment variables: which is precisely the shape a Flatpak or Android sandbox wants an engine to be, and exactly the shape the old stack wasn’t. And the crate has zero dependencies5, so it builds offline, identically, from a pinned toolchain.

Fuck. Yes.

But my favourite exhibit is about legibility, not safety, because modernising a decaying engine is the work of making intent visible. The old stack was full of behaviour that existed only as accidents (of pointer iteration; not like the kind of accident where you fall over on a freshly mopped floor). Here is one of them, now written down, from the absolutely not secret source:

/// Resolve the lemma's case the way Onym's populate_synonyms does. While listing the prime
/// sense's words it re-points the lemma at each synset word that matches it case-insensitively,
/// so the LAST such word wins: "wordsworth" becomes "Wordsworth", but a synset listing both
/// "Moon" and "moon" settles on "moon", which is why the lowercased query then sorts as an exact
/// match.

In the C, nobody chose that rule. It fell out of a loop that kept overwriting a pointer, and it shipped for fifteen years, and users’ expectations formed around it (wouldn’t life be easier without users.). Now it is a documented rule with a name, a spec section and a test. If future-me breaks it (damn him!), a fixture fails with the word that proves it broke, you numpty. That, more than any borrow checker, is what stops the next two decades of rot. And hopefully we’re all very post-computer by then living in Luxury Gay Space Communism.

And the numbers, because Rust people always ask: dumping all 145,420 headwords through the new engine takes 85 seconds on my Famework Laptop 12, output formatting included. About 0.6 milliseconds a word. The thesaurus answers faster than I can mistype (and certainly faster than I could think up 145,420 words).

Where that leaves the thesaurus

Onym 0.2.0 shipped today with the shared core inside: deb, Flatpak and AppImage, served from my own self-hosted package repos, source on my own forge (with a GitHub mirror, because discoverability is a thing and hypocrisy is a spectrum). The Android port swaps next; the same Rust core goes in through JNI, and extJWNL and its writable-directory tantrum go in the bin.

Credit where it’s due, because none of this is a solo story: Princeton’s WordNet remains a genuine public good, Debian’s maintainers kept the old library alive years past anyone’s obligation, and Artha showed everyone what the interface could be. The engine is GPL-3.0-or-later, so it’s anyone’s now. If you want a WordNet core that doesn’t need a patch stack and doesn’t phone anyone, it’s sitting there.

Which is the sovereignty bit, landed at last (I told you there was reason for my rambling). My dictionary runs on my machine, reads files I can inspect, over data the public owns, through code anyone can audit, edit, and use. In this regard, the tool is now even more robust and leans more into digital sovereignty.

And nobody is logging which words I can’t remember.

Apple’s Dictionary was a lifesaver once. This one’s mine.

Hello, Onym.

Aidan x

Footnotes

  1. for those not following at home, Linux is like Windows, MacOS, iOS or Android (well, it is also Android, but let’s not confuse things even further).

  2. notably Apple happily use your data to target you with advertising, they know they have a secret weapon in locking you into their ecosystem. They try to make it so only they can spy on you, then sell aggregate spy data to other companies (that’s where the trillions come from). Apple makes MOST of its bank from services and ad sales because they attract a “higher calibre” customer who actually spends more on products.

  3. A patch here is a small text file describing a change to someone else’s source code, applied at build time. Carrying twenty-two of them means “we change this code constantly to keep it alive, but upstream is a ghost, so we may never actually change it”. CVE-2008-2149 is a catalogued security vulnerability, in this case buffer overflows, the classic C failure where a program writes past the end of its allotted memory. From 2008. Still being patched onto every build in 2026.

  4. Pica pica is the Eurasian magpie. “Pica” the typographic unit and “pica-pica” the bird are homographs: same spelling, different words. The truncation bug meant the thesaurus ate the bird – not literally, I hope its a vegan.

  5. Zero dependencies as in zero: the crate uses the Rust standard library and nothing else. No supply chain to audit, nothing to fetch, which also happens to be a hard requirement of Flathub’s network-isolated builders. The less a tool depends on, the less it decays.