Viscosity
Review
Part IX · Rust for TypeScript developers · Day 31

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.

About 17 minutes, including the exercises and quiz
By the end you will be able to
  • 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.

Intuition

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:

Step throughWhen does the work happen?
Two pipelines: TypeScript on V8, and Rust with rustcTYPESCRIPT + NODEhello.tsstrip typeshello.jsV8parse, JIT, GCRUSTmain.rsrustccheckrustccompileCPUruns binarybefore the program runswhile it runsgreen: done · amber: happening now
Two versions of the same program. On top, TypeScript; below, Rust. Nothing has run yet. Watch WHEN each road does its work.
1/5

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.

Definition

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:

The same VM, two languages
Seconds per benchmark program, compile time included (shorter is faster)
TypeScript VMdrip-rs (Rust)trees: TypeScript VM 21327 ms, drip-rs 4858 ms (4.39×)trees21.3 s4.9 s · 4.39×equality: TypeScript VM 16501 ms, drip-rs 4220 ms (3.91×)equality16.5 s4.2 s · 3.91×binary_trees: TypeScript VM 13078 ms, drip-rs 2672 ms (4.89×)binary_trees13.1 s2.7 s · 4.89×fib: TypeScript VM 5685 ms, drip-rs 1460 ms (3.89×)fib5.7 s1.5 s · 3.89×instantiation: TypeScript VM 3570 ms, drip-rs 628 ms (5.68×)instantiation3.6 s0.6 s · 5.68×properties: TypeScript VM 2781 ms, drip-rs 795 ms (3.5×)properties2.8 s0.8 s · 3.5×invocation: TypeScript VM 2042 ms, drip-rs 577 ms (3.54×)invocation2.0 s0.6 s · 3.54×zoo: TypeScript VM 1749 ms, drip-rs 528 ms (3.31×)zoo1.7 s0.5 s · 3.31×method_call: TypeScript VM 1085 ms, drip-rs 316 ms (3.43×)method_call1.1 s0.3 s · 3.43×string_equality: both VMs refuse it (too many constants)zoo_batch: stops after 10 s of clock time, so no time to compare
Table view
programTS VM msdrip-rs msratio
binary_trees1307826724.89
equality1650142203.91
fib568514603.89
instantiation35706285.68
invocation20425773.54
method_call10853163.43
properties27817953.5
string_equalityerrorerror—
trees2132748584.39
zoo17495283.31
zoo_batch10011100041

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.

On every program that could be timed, the Rust port ran between 3.31× and 5.68× as fast as the TypeScript VM, in this one run.
A faithful Rust port of the same VM ran several times faster here. From the committed file companion/rust/results/drip-rs-bench-2026-10-31.md. Both columns include compiling the Drip program.

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_equality fails on both. It has more constants than one chunk allows, the same 256 limit in both VMs.
  • zoo_batch runs 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:

Try itHello, Rust
fn main() {
    println!("Hello, Drip!");
    let n = 35;
    println!("fib({}) is slow", n);
    let sum = 2 + 3;
    println!("2 + 3 = {}", sum);
}
Open in Rust Playground ↗

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 check type-checks without making a binary. It’s the fast one, like tsc --noEmit.
  • cargo build compiles to target/debug/hello. cargo run builds, then runs.
  • cargo build --release turns on the optimizer and writes to target/release/. Debug builds are for working, release builds for measuring: drip-rs’s numbers above are release numbers.
Definition

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.

Under the hood

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.

ExerciseTranslate a function from TypeScript

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!()
}
Open in Rust Playground ↗

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. --release turns 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. 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. 2.

    Which of these happens while a Rust binary runs?

  3. 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. 4.

    You forget the ! and write println("hi");. What happens?

  5. 5.

    Which cargo command would you run to measure how fast your program is?

  6. 6.

    drip-rs’s Cargo.toml declares a crate drip with no main, and a crate drip-rs with one. What are they?

References

  1. [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. [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. [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. [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)