Six entries. Each one is a bit of Rust that gives one answer on the server and a different answer in the browser.
The file is known-mismatches.toml, and it lives in the test suite of Topcoat, the full-stack Rust framework from the Tokio project. It was on GitHub trending this week, 192 stars in one day. I read through its source, and that small file is why I'd trust it more than most tools that promise "write it once, run it on both sides".
What Topcoat does
Topcoat renders every page on the server. A component is an async Rust function, so it can hit your database itself, with no API layer in the middle. Interactivity comes from signals and $(...) expressions: plain, type-checked Rust that the server evaluates for the first render and that also gets translated to JavaScript, so the browser can run it again without a round trip. Nothing compiles to wasm, and there's no separate client build.
This is the counter from the runtime docs, trimmed:
let count = signal(cx, || 0usize); Ok(view! { <button @click=$(|_e| count.set(count.get() + 1))>"+1"</button> <p>"Count: " $(count.get())</p> }) The click never reaches the server. When you do need the server, you mark a component #[shard] and it re-renders there whenever its arguments change, with the new HTML patched in place. It's MIT licensed, 0.12.0 shipped on November 9, and the README is upfront that it's experimental and the API will keep breaking. Getting started is cargo install topcoat-cli, then topcoat new.
I did not install it. This Mac has no Rust toolchain and the releases ship no binaries, so everything below about Topcoat comes from reading its source, docs and issues. The JavaScript I ran is my own.
One expression, two compilers
Here's what happens to a $(...) at build time. The macro walks the Rust syntax tree and writes two programs side by side: the Rust you wrote, typed against stand-in "surrogate" types, and a string of JavaScript that calls a matching set of TypeScript surrogates in the browser. count.get() + 1 stays a + in Rust and becomes .add(...) in the JavaScript. A Rust block becomes an arrow function that is called on the spot, since JavaScript has no block expressions. In the browser, the runtime turns the string into a function with new Function.
So every operation exists twice, and each browser copy has to fight JavaScript's defaults. len() on a string can't use .length, which counts UTF-16 units, so the browser encodes the string with TextEncoder and counts bytes like Rust does. trim() gets a hand-written whitespace class, because, as a comment in its source says, JavaScript's trim strips U+FEFF and keeps U+0085, the reverse of Rust. String comparison walks code points instead of UTF-16 units.
I checked the JavaScript side of that in Node:
"\u{1f980}".length (plain JS): 2 UTF-8 bytes via TextEncoder: 4 "\uFEFFhi".trim().length (plain JS): 2 "\u0085hi".trim().length (plain JS): 3 A crab emoji is 2 long in plain JavaScript and 4 bytes in Rust. On the July launch thread on Hacker News, jdw64 guessed that len() would come out different on the two sides. Today it doesn't, and a test pins it.
The harness that keeps them honest
That test lives in a crate called coherence. You write coherent!(text.len()). The harness compiles the expression once, runs the Rust in the test process and the generated JavaScript in V8 with the real browser runtime, then compares the two results down to float bits and negative zero. A panic in Rust has to be matched by a panic in JavaScript.
The string tests push 22 awkward inputs (empty, a NUL byte, a combining accent, U+10FFFF, the crab) through every method and every pairwise comparison. By my count that is 4,488 cases from one test function, out of 68 test functions in the crate.
The timeline is worth a look. In July, a user opened issue #275 arguing that the two implementations could drift apart with no compiler error, and listed five bugs where they had. AmeinEskinder, who was behind four of the five, replied that one only surfaced under a differential test: a float that sits exactly between two short decimal spellings, a tie Rust and ECMAScript break in opposite directions. It turned up 3 times in the first 20,027 random doubles tested. The coherence crate landed on September 15.
Six things that still disagree
A harness that only ever passes would be a nice badge. This one also records what fails. A known mismatch is a named case with the exact Rust result and the exact JavaScript result. If the two sides ever start agreeing, the test fails and tells you to delete the entry. The six today:
- A captured
NaN, or a captured infinity, reaches the browser as unit, since JSON has no non-finite numbers (two entries). -
Some(())reads asNonein JavaScript, which usesundefinedfor both. -
returninside anifreturns from the wrong function. -
breakorcontinueinside anifdoesn't compile in the browser at all (two entries).
The return case is the one I'd watch, because it fails quietly. { if true { return 1.0; } 2.0 } is 1.0 in Rust and 2.0 in the browser. The if and the block each become an arrow function, so return leaves the inner one and the outer one carries on. I rebuilt that shape by hand, with plain values in place of the surrogates:
const lowered = () => { (() => { if (true) { return 1.0; } })(); return 2.0; }; lowered(); // 2 The break version throws SyntaxError: Illegal break statement in Node, the same string the baseline file records. A known bug in a file beats a known bug in someone's head: the next person to touch the compiler trips over it on purpose.
What the harness can't see
The harness docs are clear about its limits. It compares final values, not clicks, rendered HTML or side effects. And since both sides run surrogate types, agreement proves the server and browser match each other, not that either one matches ordinary Rust.
Some bugs fall outside it. Issue #543: capture a &i32 in a click handler and it compiles fine, then throws in the browser, because numbers there have no deref. Issue #484: new Function counts as eval, so a page needs 'unsafe-eval' in its Content Security Policy, and a strict policy blocks every reactive expression. Both are open. The vocabulary is also small on purpose. There's no match, and the operator list in the macro source has no && or ||.
pikaju, who wrote most of the commits, was blunt in #275: the current system is flawed, and "In my opinion we shipped it prematurely." The goal described there is a small language for flipping a class, setting a signal or calling a procedure, not client-side Rust. That is the bar to judge it by, and the coherence suite is how they hold it.
Ask for the mismatch list
Every tool that runs one source in two places makes the same promise. Topcoat is the first one I've read that ships the receipts: a test that runs both sides, and a file that names where they still split. If you're choosing a layer like that, for Rust or anything else, ask to see its list. No list doesn't mean no mismatches.
Have you hit a case where your server and your browser computed the same thing differently, and how long did it take you to notice?
