NOTE 0018 / SYSTEMS
Why Rust
Not for speed. I write the cores in Rust because everything I build makes the same promise — that the same input produces the same output, and that you can check it — and that promise is not about how fast a program runs. It is about how precisely I can say what it will and will not do.
Money must not be a float
Calybris is a decision kernel: catalog, policy and request in; a decision and a replayable audit bundle out. The whole product is that replay. Months from now somebody re-runs the log and gets the identical decision, or the system is worthless.
Floating point breaks that quietly — not because it is inaccurate, but because it is inaccurate in ways that depend on order. Sum the same numbers in a different sequence and the total moves. The difference stays invisible until it is a budget one cent over, authorizing something it should have refused.
So the numeric path has zero floating point in it. Values are fixed-point integers. Where floats are unavoidable in analysis code elsewhere, integers accumulate in i128 and floats accumulate in row order with Neumaier compensation, so a total cannot silently go negative and cannot move with the batch size.
A language that lets me say "there is no float on this path" — and lets the compiler hold me to it — is doing something for me that a comment cannot.
The concurrency half is the same argument. A budget check and a debit cannot be two separate observations; between them, another thread can spend. Rust let me model the transition as one state-machine step and then use Loom to enumerate scheduler interleavings around the shared ledger. The property being tested is not "the lock looks correct". It is that no explored execution authorizes more budget than exists.
A bound you can state
ProofFrame validates datasets that may not fit in memory. Its defaults are 512 MiB of memory and 4 GiB of temporary storage, the memory budget is charged before each allocation rather than observed afterwards, and a scan that would exceed it fails rather than growing. Set spill="never" and it refuses instead of touching the disk at all.
That is a commitment about behaviour under pressure, made in advance. I do not know how to write it honestly about a Python pipeline — not because Python cannot be careful, but because the allocation is not mine to charge.
The interesting question about a validator is never how fast it runs on good data. It is what happens when the data is larger than the machine, the schema is wrong, or the temporary directory is read-only. Each of those has to be an error with a name, reachable from a test.
One engine, three surfaces
This is the argument that actually convinced me, and it took a while to see. ProofFrame runs as a command-line tool, as a Python package through PyO3, and in a browser tab compiled to WebAssembly. The browser demo is not a reimplementation and not a recording. When it renders a review, it calls the crate's review_html — the same compiled function the CLI writes its file with. No HTML is assembled in JavaScript. A review saved from a browser is byte-for-byte the document the CLI produces.
Three delivery surfaces, one definition of what the answer is. I have written the other version, where a Python implementation and a JavaScript one are supposed to agree — and they do, until an edge case arrives and then only one of them is right. Keeping two implementations in sync is not a hard problem. It is an endless one.
WebAssembly is where this pays visibly: the analysis engine runs inside the visitor's tab, and no file is uploaded anywhere. That is not a performance feature. It is a privacy property that happens to be available because the core was portable.
The bug Rust did not catch
So that this does not read as a pitch, the counter-example — which cost me a conclusion. In my trading research the cost table had a commission constant of 70_000 where the comment beside it said 7_000_000. A hundred-fold error, in a number that decides whether a raw-spread account is cheaper than a retail one. With the wrong constant, it was. Corrected, the raw account is the dearer of the two and a whole line of reasoning reversed.
Rust saw an i64 assigned to an i64 and was satisfied, correctly. The type system cannot catch a unit error unless you make the unit a type, and I had not. There is no borrow checker for "this number is in microcents per centi-lot."
What caught it was a test that pins the unit. Not the language.
Rust removes one category of mistake and leaves the category above it untouched. Memory safety and data-race freedom are real, and they were never the errors about to cost me money. Wrong constants, wrong units and wrong assumptions were, and those are language-independent. If anything, a compiler that catches the small things makes it easier to believe the big ones are fine.
Where I do not use it
The clearest version of that boundary is inside one small repository. ForecastEdge scores two years of archived forecasts for thirty cities, and it is Python on one side and Rust on the other. Fetching the archive is entirely waiting on a network: nothing about the language reaches the outcome, and Python is the shortest path to done. Scoring is 1.68 million bootstrap evaluations, and every figure published from it has to come back the same on a re-run. Waiting on one side, guaranteeing on the other — the line falls where the work changes kind, not where I have a preference.
The Python bindings follow the same logic. People who validate data work in Python, and asking them to leave the notebook is asking them not to come. Rust sits underneath, where it changes what can be guaranteed; Python sits on top, where it changes whether anyone shows up.
Rust also does not remove constraints; it makes them visible. The WebAssembly target has no filesystem and the exact-set path spills to a temporary directory, so in the browser the exact rules are refused before they reach the engine rather than quietly approximated. Correct behaviour, and still a missing feature. Portability bought a surface, not a free lunch.
So, when
The rule I have settled on, after building this stuff rather than before:
Reach for Rust when a wrong answer is expensive and when you need to say, in advance and in writing, what the program will refuse to do.
Speed sells too; I am not pretending otherwise. But speed is a number you can go back and improve, and often the cheapest way to improve it is to stop doing something rather than to change languages. A guarantee is not like that. Determinism, a bound that holds under pressure, one implementation behind several surfaces — you either build those in at the start or you do not have them, and no later optimisation pass adds one.
Everywhere else — glue, fetching, exploration, the part where you do not yet know what you are building — the compile time is a tax with nothing on the other side. I pay it in the core and refuse to pay it at the edges. Where that line falls is the actual design decision.