121. Why Rust? Hello, cargo
You built a VM in TypeScript and a compiler that rearranges memory. Track B rebuilds them in a language where memory is yours to arrange, and where the compiler checks your work before anything runs.
- Say what Rust gives a VM author that TypeScript can’t, and what it costs.
- Read the drip-rs benchmark honestly, including what it does not show.
- Create, build and run a project with cargo, and name the files it makes.
- Tell a binary crate from a library crate.
Two garbage collectors deep
Think back to chapter 58. Your Drip VM is a TypeScript program, so it runs on V8, and V8 has a garbage collector[3]. Yet you still had to write a second collector, a mark and sweep over the VM’s own object list, because only the VM knows which Drip objects are still reachable. Two collectors, stacked.
Then think back to chapter 22. Every JavaScript object carries a hidden header, and V8 decides how numbers are stored. Viscosity, the compiler you finished in Part VIII, exists because JavaScript gives you no direct say over memory layout. It has to trick the engine into a struct of arrays, using typed arrays.
Track B starts again in a language where neither trick is needed.
TypeScript is like a hotel. The staff (the engine) clean your room, decide where your luggage goes, and tidy up after you leave. Rust is like owning the house. You decide where everything goes, and nobody cleans up for you, but a very strict inspector checks the plans before you’re allowed to move in.
What Rust is, and what it costs
Rust compiles ahead of time to machine code, has no garbage
collector, and still refuses to compile programs that would read freed
memory (outside code marked unsafe, chapter 141)[1]. It manages that with a set of
ownership rules, checked by the compiler. They are the heart of this
part of the book, and they begin in chapter 124.
Here is the same idea on the two roads. Step through it and watch when each one does its work:
V8 does a lot while your program runs: it parses, interprets bytecode with Ignition, and compiles hot code[2]. A Rust program did all its compiling before it started.
Rust is a compiled systems language. Its compiler, rustc, checks
types and memory-safety rules for the whole program, then produces
native machine code. The running program has no garbage collector and no
JIT: memory is freed at points the compiler works out in advance.
The costs are real, and you should hear them now. The compiler rejects programs that TypeScript would happily run, and at first it will feel like arguing with it. You wait for a full compile before every run. And some designs that are trivial with a GC, such as many objects all pointing at each other, take more thought.
The same VM, measured
Is it worth it? The book’s companion code has an answer for one program
you know well. In Part X you’ll port the Drip VM to Rust, as
Crafting Interpreters moves from Java to C[4]. The
port, drip-rs, prints exactly what the TypeScript VM prints, byte for
byte. Here it is on the benchmark programs from chapter 44:
Table view
| program | TS VM ms | drip-rs ms | ratio |
|---|---|---|---|
| binary_trees | 13078 | 2672 | 4.89 |
| equality | 16501 | 4220 | 3.91 |
| fib | 5685 | 1460 | 3.89 |
| instantiation | 3570 | 628 | 5.68 |
| invocation | 2042 | 577 | 3.54 |
| method_call | 1085 | 316 | 3.43 |
| properties | 2781 | 795 | 3.5 |
| string_equality | error | error | — |
| trees | 21327 | 4858 | 4.39 |
| zoo | 1749 | 528 | 3.31 |
| zoo_batch | 10011 | 10004 | 1 |
From companion/rust/results/drip-rs-bench-2026-10-31 (.md and .json): rustc 1.97.0 (2d8144b78 2026-07-07); TypeScript on Node v22.23.4; Intel(R) Xeon(R) Processor @ 2.10GHz; 2026-10-31. drip-rs: best of 3 runs, each a fresh process, release build. TypeScript VM: 1 run per program, from chapter 44’s results file. Same shared cloud VM, so expect noise.
Read it with chapter 24’s care:
- Both ran on the same shared cloud VM, on the same day. drip-rs is a release build, best of three runs. The TypeScript numbers are the one run per program that chapter 44 used. This is a first look.
- This is one port, not “Rust versus JavaScript”. Different code, or a JIT that suits a program better, could change every bar.
string_equalityfails on both. It has more constants than one chunk allows, the same 256 limit in both VMs.zoo_batchruns for a fixed ten seconds. The results file adds a hand count: drip-rs finished 3060 batches, the TypeScript VM 844.
Hello, Rust
Here is a complete Rust program:
fn main() {
println!("Hello, Drip!");
}
fn main() is where every Rust program starts. There is no top-level
code as in a JavaScript file. println! plays the part of
console.log. The ! marks it as a macro: code that writes code at
compile time. Forget the ! and the compiler tells you so. This is the
real output of rustc 1.97:
error[E0423]: expected function, found
macro `println`
--> e1.rs:2:5
|
2 | println("Hello, Drip!");
| ^^^^^^^ not a function
|
help: use `!` to invoke the macro
Rust’s error messages are long and specific. Read them to the end: the help:
line often holds the fix. Inside the string, {} marks a gap that
println! fills with the next argument. Try it:
fn main() {
println!("Hello, Drip!");
let n = 35;
println!("fib({}) is slow", n);
let sum = 2 + 3;
println!("2 + 3 = {}", sum);
}cargo: Rust’s npm
You can compile one file with rustc main.rs. Nobody does, past day
one. Real projects use cargo, which plays the parts of npm, tsc
and your test runner at once[1].
$ cargo new hello
$ cd hello
$ cargo run
Compiling hello v0.1.0
Finished `dev` profile
Running `target/debug/hello`
Hello, world!
(Output from cargo 1.97, trimmed.) cargo new made two files:
hello/
├── Cargo.toml
└── src/
└── main.rs
Cargo.toml is the package.json: a name, a version, an edition, and
a [dependencies] list. Packages come from crates.io, as npm packages
come from the npm registry.
[package]
name = "hello"
version = "0.1.0"
edition = "2024"
[dependencies]
The edition picks a version of the language’s rules. New cargo projects get 2024. This book’s runnable examples use 2021, and nothing in the next chapters differs between the two.
The everyday commands:
cargo checktype-checks without making a binary. It’s the fast one, liketsc --noEmit.cargo buildcompiles totarget/debug/hello.cargo runbuilds, then runs.cargo build --releaseturns on the optimizer and writes totarget/release/. Debug builds are for working, release builds for measuring: drip-rs’s numbers above are release numbers.
cargo is Rust’s build tool and package manager. A crate is the
unit rustc compiles in one go. A binary crate has a main and
becomes a program. A library crate has no main; other crates use
it. A package (one Cargo.toml) holds one or more crates.
drip-rs is a nice example. Its Cargo.toml declares a library crate,
drip, holding the scanner, compiler, VM and collector, and a binary
crate, drip-rs, a short main that reads a file and calls the
library.
Viscosity rearranged arrays of objects into typed-array columns because
JavaScript hides layout. In Rust, a Vec of structs and a struct of
Vecs are both ordinary code you write, with no object headers in the
way. Chapter 140 builds the struct of arrays by hand. Part XI then
rebuilds Viscosity itself in Rust: still a compiler for JavaScript, but
one whose own IR lives in memory it lays out on purpose.
Your turn
Rust exercises run on the official Rust Playground, so they take a few
seconds. The tests are #[test] functions; you’ll write your own in
chapter 136. For now, read the starter: it looks a lot like TypeScript.
i64 is a 64-bit whole number, and -> i64 means “returns one”.
todo!() is a macro that crashes with “not yet implemented”, a handy
placeholder.
Here is a TypeScript function:
function secondsIn(hours: number): number {
return hours * 60 * 60;
}The Rust version is already declared for you. Replace todo!() with a body that returns the number of seconds in hours hours. The shape is the same as in TypeScript, down to return and the semicolon.
fn seconds_in(hours: i64) -> i64 {
todo!()
}What you learned
- Rust compiles to machine code ahead of time, with no garbage collector and no JIT. The compiler checks memory safety before the program runs. The price is a stricter compiler and slower builds.
- In this book’s run, the Rust port of the Drip VM was several times faster than the TypeScript VM. One run, one port, one machine.
fn main()is the entry point.println!is a macro.- cargo creates, checks, builds, runs and tests projects.
--releaseturns on optimization. - A crate is one compilation unit: a binary (with
main) or a library.
Next, variables: Rust’s let is TypeScript’s const, and number
splits into a dozen types.
Check your understanding
0/6- 1.
Your TypeScript Drip VM has its own mark-and-sweep collector. Why did it need one, even though V8 has a garbage collector?
- 2.
Which of these happens while a Rust binary runs?
- 3.
In this book’s run, drip-rs was 3.89× as fast as the TypeScript VM on
fib. What is the fairest conclusion? - 4.
You forget the
!and writeprintln("hi");. What happens? - 5.
Which cargo command would you run to measure how fast your program is?
- 6.
drip-rs’s
Cargo.tomldeclares a cratedripwith nomain, and a cratedrip-rswith one. What are they?
References
- [1] Steve Klabnik, Carol Nichols, Chris Krycho (2026). The Rust Programming Language (3rd edition). No Starch Press; free online at doc.rust-lang.org/book. doc.rust-lang.org/book/ (checked 2026-10-31)
- [2] Ross McIlroy (2016). Firing up the Ignition interpreter. V8 blog (v8.dev), 23 August 2016. v8.dev/blog/ignition-interpreter (checked 2026-10-31)
- [3] Peter Marshall (2019). Trash talk: the Orinoco garbage collector. V8 blog (v8.dev), 3 January 2019. v8.dev/blog/trash-talk (checked 2026-10-31)
- [4] Robert Nystrom (2021). Crafting Interpreters. Free web edition at craftinginterpreters.com (© 2015–2021); source, text and test suite at github.com/munificent/craftinginterpreters. github.com/munificent/craftinginterpreters (checked 2026-10-31)