01
HIR, THIR, and MIR: The Same Rust Function at Three Compiler Stages
One small Rust function followed through real HIR, THIR, and MIR output, with a practical explanation of what each compiler representation is for.
Async, types, memory, and compiler behaviour
Practical explanations of the Rust behaviour that only becomes obvious after reading compiler output, measuring a program, or working close to the language and its tools.
The same Rust writing is also mapped as one system, from compiler internals to async runtimes, in the Rust Systems Atlas.
Rust gives us strong guarantees, but the most useful mental models often sit below the surface syntax. Futures become state machines. A harmless-looking local changes a future's size. Variance, drop checking, pinning, and pointer provenance shape which abstractions are actually sound.
This series makes those mechanics observable. Each article starts from a concrete question, builds the smallest useful model, and then connects it to decisions we make in real code.
01
Use expanded code, type information, layouts, MIR, diagnostics, and small experiments instead of relying on folklore.
02
Language rules matter because they change API boundaries, memory use, concurrency, and the safety of an abstraction.
03
Prefer focused programs and commands that a reader can run, change, and use to disprove the explanation.
Entry points into this subject, ordered as a reading path.
01
One small Rust function followed through real HIR, THIR, and MIR output, with a practical explanation of what each compiler representation is for.
02
Two identifiers, two different jobs. DefId names a definition across the whole compiler pipeline and across crates. HirId points to a node in the syntax tree of the current crate. Mixing them up is the first stumbling block when reading rustc source code.
03
A practitioner’s deep dive into ops, resources, and the V8 bridge in Deno’s Rust core.
04
A deep dive into what actually happens when you write #[tokio::main]. We'll tear apart the macro, expose the state machines, and reveal the work-stealing scheduler hiding beneath five lines of code.
Multi-part work with the code and measurements that produced its evidence.
10 parts
Ten parts on what a type is and where it goes: sets of values, layouts and padding, erasure and monomorphization, the bits the processor actually sees, and what the checker knows that the binary forgets. Small programs in Rust, C, TypeScript and Python, with one script that reproduces every output.
3 parts
A measured diagnostic tree for slow Rust builds: cargo --timings for the shape, the fingerprint log for unexpected rebuilds, -Ztime-passes for the phase inside one crate, and what splitting a workspace into more crates actually changes.
3 parts
Five async Rust problems reproduced in one small tokio program: an oversized future, a future that is not Send, a blocking call, a select! loop that loses data, and a shutdown that hangs. Then the cost of async fn through dyn Trait, and cleanup when Drop cannot await.
Through the layers
Zero-copy scanning still reads every inspected byte. It removes a second materialization in memory. I use streaming Aho-Corasick to explain automaton state, chunk boundaries, DFA dependency chains, prefilters, and honest performance measurement.
Through the layers
A source-to-diagnostic walkthrough of trait goals, parameter environments, candidates, associated-type normalization, and nested obligations.
Through the layers
A Rust future is Send only when every stored state is safe to move across threads. Trace captures and locals across await to fix spawn errors.
Through the layers
Rust async functions store every value needed after an await. Learn to measure future size, identify saved fields, and reduce it without boxing everything.
Through the layers
How I built a custom code execution engine backed by a Rust/Axum microservice, embedded directly in blog articles. Covers the security model, subprocess isolation with setrlimit, HMAC request signing, and the tradeoffs behind every decision.
Through the layers
Measured Rust enum layouts showing how invalid payload values encode Option discriminants, which optimizations are guaranteed, and what repr changes.
Through the layers
A Rust pointer carries an address and provenance. Learn addr, with_addr, map_addr, exposed provenance, and sound pointer-tagging patterns.
Through the layers
A tested Rust-and-C FFI boundary covering ABI layout, pointer validation, ownership, panics, error reporting, callbacks, versioning, and cross-language harnesses.
Through the layers
PhantomData stores no value, yet changes variance, Send, Sync, ownership, and drop checking. Learn to choose a marker from the real contract.
Through the layers
Pinning a Rust struct does not pin every field automatically. Learn structural pinning, safe projection, Drop obligations, and Future wrappers.
Through the layers
The usual Fn(&T) -> Fut bound cannot describe a future borrowing from each call. See how AsyncFn enables lending callbacks on stable Rust.
Through the layers
Follow Rust drop scopes through if let, match, and tail expressions, including the Edition 2024 changes affecting locks and temporary borrows.
Through the layers
A span comparison harness showing why generated Rust errors point at the macro invocation, how to preserve input spans, and how to test diagnostics with compile-fail fixtures.
Through the layers
Rust identifiers carry syntax context as well as text. Learn macro_rules hygiene, $crate paths, and procedural-macro spans for reliable expansion.
Through the layers
Trace go to definition from a cursor token through syntax, macro expansion, name resolution, semantic identity, and finally back to an editable source range.
Through the layers
Rust 1.97 made v0 symbol mangling the stable default. Compare real nm output, read the encoding, and know what changes for profilers, debuggers, and ABI boundaries.
Through the layers
Rust 1.97 forwards successful linker output through a special lint. Learn who produced the message, why -D warnings ignores it, and how to investigate it.
Through the layers
When you write let s: &str = "hello world", there are really two things in one line, living in two different places. Here is what each one is, where it lives, and why that explains lifetimes.
Through the layers
Learn how Rust's Acquire and Release memory orderings work, their importance in concurrent programming, and how they enable lock-free synchronization between threads.
Through the layers
Pattern count alone cannot predict fast string search. I show how rare bytes, short literals, prefixes, alphabet size, and candidate density shape SIMD-assisted Rust performance.
Through the layers
Linear-time search does not promise constant speed. I explain how automaton size, cache behavior, construction, match density, and pattern shape change real Rust throughput.
Through the layers
A prefilter can skip most automaton work, but it is heuristic rather than free. I show how candidate frequency and pattern shape decide whether it helps a Rust search.
Through the layers
An automaton can recognize a cross-chunk match with little state, yet a streaming API may still need bytes. The missing distinction is recognition state versus output state.
Through the layers
A pattern may begin in one input chunk and finish in the next. I explain the state, offsets, and split-point tests needed to keep a Rust streaming scanner correct.
Through the layers
Rust's Aho–Corasick crate can build three different automata. I compare their construction cost, memory layout, search behavior, and the evidence needed before forcing one.
Through the layers
The same patterns and haystack can produce different correct matches. I make Rust's three Aho–Corasick match kinds concrete and show how to choose by product semantics.
Derived state
Rust's incremental cache is a dependency graph, not a saved compiler process. Learn what an edit dirties, how green nodes are recovered, and why Cargo may still run rustc.
Derived state
A practical model of rustc queries, providers, memoization, dependency tracking, cycles, and red-green incremental reuse.
Derived state
Cargo build scripts are cached from declared file and environment inputs. Use rerun-if rules, verbose evidence, and a rebuild matrix to remove accidental rebuilds.
Derived state
A concrete Cargo dependency graph showing when features unite across workspace packages and when host, target, and platform edges stay separate.
Derived state
Rust 1.97 lets Cargo deny cached lint warnings without rebuilding crates under different RUSTFLAGS. Here is the behaviour and the migration path.
Derived state
A tested walkthrough of monomorphization collection, concrete generic instances, codegen-unit partitioning, compile-time parallelism, and binary-size trade-offs.
Derived state
Cargo resolver 3 prefers dependency releases compatible with your declared rust-version. See where fallback helps, where it cannot, and how workspaces change the result.
Interrupted execution
Async cancellation usually means dropping a future after Pending. See how select loops lose partial progress and how to design restartable operations.
Older, introductory or narrower pieces on the same subject.
Engineering context
I work on systems where compiler behaviour, runtime constraints, correctness, and performance are part of the design, not cleanup after it.