Building questions that teach

The hardest part of wat.rs wasn’t the software — it was coming up with content worth quizzing about. Soliciting cursed Rust trivia on Mastodon and Bluesky turned up plenty of interesting facts, but most weren’t quiz material.

The goal was questions that teach something even to people who answer them wrong. Starting from simple optimizations like turning divisions:

fn f(x: u64) -> u64 { x / 2 }

...into shifts:

fn f(x: u64) -> u64 { x >> 1 }

During preparation, a happy accident surfaced something unexpected:

fn f(x: u64) -> u64 { x / 5 } // optimizes to: fn f(x: u64) -> u64 { ((x as u128 * 0xCCCCCCCCCCCCCCCDu128) >> 66) as u64 }

Compiler Explorer became the primary workbench. Each candidate was checked on CE, then translated back into Rust for the quiz format:

Amos

The best questions built up a pattern. Something innocent clearly works, another example works just as well, and then one small tweak breaks everything. A series of floating-point questions demonstrated this nicely:

x / 1.0 // optimizes to x

Some cases behave:

(x / 1.0) / 0.0 // optimizes to x / 0.0

Others, surprisingly, don't:

(x / 3.0) / 0.0 // ⚠️ does NOT optimize to: x / 0.0

The explanation for that one — and several "is this memcpy?" cases — wouldn't click until playtesting later. All questions went into a spreadsheet categorized by arithmetic, undefined behavior, dead code elimination, constant folding, loops and trivia.

Rapid prototyping with Dioxus

Once the question bank was full enough for a 30-minute session, it was time to build. Dioxus came highly recommended, and its dx tooling made the initial developer experience smooth.

The dx serve --web command, ran in a terminal.

Shiny!

Dioxus LiveView was tempting but turned out to be in poor shape; the full-stack approach was the right call. The 0.7.0 release candidate offered subsecond hot patching, though it took a week to realize it needed the --hot-patch flag on the dx CLI. Even when enabled it was fast but crashy, true to the release notes.

For simple changes, everything works fine! And it's real quick, compared to... everything else in the Rust ecosystem.

Batch code changes, wait for recompile, switch to the browser, restore state, inspect the result — that loop was tedious compared to a Svelte 5 setup with Vite hot reload under a second. But what iteration speed cost, compile-time confidence repaid. Prior CMS experience in Rust translated directly: pulldown-cmark for Markdown and tree-sitter-highlight for syntax highlighting.

Hardcoding slides turned out to be a dead end. A flexible format emerged instead: a single Markdown document, each slide delimited by three dashes, with quiz questions as GitHub-flavored checkbox lists where the checked box marks the correct answer.

# Title --- Question? 1. [x] Yes 1. [ ] Maybe 1. [ ] No --- (The last slide is replaced by results)
Amos

Notably, Markdown doesn't require sequential numbers in ordered lists — every item can be numbered one and the renderer handles ordering.

Typographic scaling on large screens was solved with vw units (viewport width, one hundredth of the viewport width). It's not a practice worth recommending and zoom behavior gets odd, but it did the job:

font-size: 3vw;
Cool bear

Server state and room mechanics

Unlike typical presentation tools, all state lives on the server. The host creates a room, which opens on a given slide. Any event — player joining, voting, navigating slides — broadcasts the entire state to every connected participant. Sending only diffs would have been more efficient, but there wasn't time to make that reliable, and Rust is fast enough.

Security was minimal but sufficient for the conference: a hardcoded password gates host access. Player names are auto-generated from random adjectives and animal names. Room IDs — four-letter codes — get filtered for profanity to keep things stage-safe.

Cool bear

A question became apparent: when is a slide eligible for votes? Navigating to a slide with zero votes meant it was open. Playtests with up to 50 people passed flawlessly. The live session was different: a forward navigation let one person vote, a backward navigation locked the slide permanently for everyone else.

Amos

That bug soured several attendees and became the main piece of post-presentation feedback.

From local build to live audience

Once the quiz was playable, the next step was packaging it for deployment. The common advice for Rust web apps — multi-stage Docker builds with cargo-chef — didn't hold up in practice here. Dioxus manages its own target directories, so pre-building dependencies with cargo-chef offered little benefit. On top of that, dx build insists on reinstalling wasm-opt and wasm-bindgen even when they are already available in $PATH.

I dropped cargo-chef entirely in favor of a cache mount:

# Create the final bundle folder. Bundle always executes in release mode with optimizations enabled RUN --mount=type=cache,target=/app/target \ dx bundle --web --release && \ cp -rfv /app/target/dx/wat/release/web /app/

That turned out to be noticeably faster for local builds. The app itself went onto a Kubernetes cluster I already run for my website, which made deployment almost routine. I also registered the domain wat.rs through the Istanco registry, and it was time for the first playtests.

A screenshot of the ISTANCO.RS website showing a research for the domain I like bars ending in dot R S showing that the prices are like twenty-three dollars sixty for RS domains.

Aurora Nockert and Lukas Wirth joined me for one-on-one video calls. They found no major bugs, but their feedback helped me make several questions clearer and more technically accurate.

Load testing without spoilers

With the conference approaching, I worried about whether the game would hold up with hundreds of concurrent players. That meant a load test — but I didn't want to spoil the real quiz for anyone. So I assembled a second quiz from leftover spreadsheet questions and, when those ran out, went through the Rust compiler's documented error messages one by one hunting for interesting material.

The test ran at 10 p.m. Paris time on twitch.tv/fasterthanlime, with Luuk Wester co-hosting. We held roughly 40 consistent players and generated plenty of chat arguments along the way.

A screenshot of the live stream, showing the quiz in the background, and two faces on the right (Luuk and I).

A meetup preview and a reconnect bug

The night before EuroRust 2025, I spoke at a Paris Rust meetup and asked to run the test quiz afterward. Waffle co-hosted; Oli came on stage to explain const-related details, and the discussion got heated enough that Rust Project contributors were arguing in the audience. The quiz itself surfaced a real bug: when players' phones fell asleep and reconnected, they received a new randomly generated player ID and lost their scores.

On the first conference day, I fixed the reconnect problem and made it possible for the host to reconnect too — a reload mid-presentation would otherwise have wiped everyone's points. I also added QR code generation so the audience could join before the talk started.

Mara Bos ran another playtest and provided the explanation for one tricky question:

fn f(x: f64) -> f64 { (x / 3.0) / 0.0 } // does NOT optimize to: fn f(x: f64) -> f64 { x / 0.0 }
Cool bear

The reasoning: 0 / 0 is NaN, and x might be non-zero but small enough that dividing by 3 rounds down to zero. The first division by 3 therefore can't be optimized away.

Amanieu took a second playtesting session and stayed up late on a different puzzle. The next morning he came back with the answer: LLVM's optimization pass order is the culprit. The "memcpy recognition" pass runs after the "remove useless assignments" pass.

// this does NOT optimize to memcpy pub fn naive(src: &[u8], dst: &mut [u8]) { assert!(src.len() >= dst.len()); for i in 0..dst.len() { dst[i] = 0; dst[i] = src[i]; } }

Last-minute features

Day two of the conference was all software and quiz content. The generated player names bothered me — I wanted credit where it was due — so I added "Log in with GitHub". The assumption was that nobody would bother creating a fake account with profanity just to game the leaderboard, and real accounts give players a stake in their scores.

Screenshot of the quiz interface on a phone.

The questions also showed up on phones, to make it easier to vote.

The other slides were only on the big screen.

The OAuth implementation was about as rough as it gets: profile information lived client-side and could have been spoofed by anyone determined enough. As far as I can tell, nobody tried.

I also added small quality-of-life touches: entering the fourth character of a room code now auto-joins, and since the first day had no clicker or remote, I built host reconnection plus swipe gestures so my phone could serve as the slide remote.

Connection stability remained the biggest worry. Dioxus 0.7.0-rc.1 had come out days earlier and advertised automatic websocket reconnection, so I spent a few hours porting everything to it. It may work as described, but it wasn't enough in my testing — at the last minute I added logic to reload the page on websocket errors or when pings went unanswered for a few seconds.

A photo of docs.rs with WebSocketOptions showing a with_automatic_reconnect method.

Why give me hope like this

Showtime

The final deployment landed 40 minutes before going on stage with James. It all went smoothly, and the audience had a good time with the quiz.

Several people asked whether I'd open-source the quiz software for use at Rust meetups. I'm open to the idea but want to polish it further first.

Paris was a positive experience, and other conferences have already expressed interest. I'll be running another quiz at RustLab 2025 in Florence, Italy, from November 2 to 4 — tickets are still available.