Playground
How the Playground works
Your Rust program, compiled and run inside your browser, turned into the same step-by-step memory walkthrough as the visualizers
What it does
The Playground takes a Rust program, runs it, and stops after every line to look at memory: each stack frame and its variables, every heap block, where each pointer points, and what the program has printed. Each stop becomes one step, drawn the same way as the built-in visualizers
- Write or paste a program, or start from the one in the editor: a small store with
staticitems, string literals, structs that ownStrings, and a&mutborrow. Reset brings it back, and the copy button in the editor's corner copies whatever is in it - It runs from
fn main, like any Rust program, so a program without one gets a note instead of a walkthrough. A lecture file of separate functions needs amainthat calls the one you want to see - Or open one from elsewhere on the site: Open in Playground on a visualizer, or Visualize in Playground under a program in these docs. It arrives in the editor and generates straight away
- Press Generate visualization, or ⌘/Ctrl + Enter. While it works, the right side shows each stage: loading the engine, compiling and running, drawing the steps
- Step through it with the arrows, the step numbers or auto-play, like any visualizer. The last step is always Program ends:
main's frame is popped and the stack is empty - Generating the same code again is instant: the Playground remembers your last 20 programs, so the button says Show visualization when nothing has changed
The engine
The engine is Aquascope, a research tool from Brown University. It's the real rustc front end plus Miri, Rust's interpreter for finding undefined behavior, compiled to WebAssembly. Miri runs the program one MIR statement at a time, and Aquascope records what memory looks like at each pause
Because Miri checks every memory access, the Playground also catches real undefined behavior: a use-after-free, or a read past the end of a buffer, stops the walkthrough with a step that explains what went wrong
Reading the diagram
Aquascope's raw output is a list of memory snapshots. The Playground turns each one into the visualizers' picture, and works out what changed since the step before to write its title and explanation. They come from fixed templates, so the text never claims anything the diagram doesn't show
The stack:
- One frame per call, the running one on top. Recursion stacks a frame per call, and a run of returns reads as one step: "3 calls to
factorialreturn" - A struct gets a slot per field, so you see how its fields sit side by side. A variable declared but not set yet reads
[???] - A moved variable fades out, and comes back once it's given a new value. A shadowed one (
let b = …; let b = …;) stays, drawn asb₁ CellandRefCellshow the value they hold; RefCell's borrow counter isn't drawn. AHashMaporHashSetis one slot with its length- A line that changes nothing still says what it did: an
iforwhilereads "Checkn <= 1", a passingassert_eq!reads "assert_eq!holds", and a print shows the text it wrote
The heap:
String,VecandBoxbecome.ptr,.lenand.capslots with an arrow to their block. Spare capacity shows as empty cells, and a push reads as a write into it- Each block keeps one made-up address for as long as it lives, and a freed address is never reused in the drawing.
x = Box::new(10)frees the old Box and allocates a new one;*x = 10writes into the same Box;mem::swapswaps the pointers, not the data - A
VecorStringthat outgrows its buffer moves to a bigger block, and the old one is shown freed.shrink_to_fitmoves it to a smaller one clone()makes a new block with a copy of the contents.Rc::cloneandArc::cloneshare the one block instead, and itsstrongcount goes up- A block nothing points at any more that was never freed is shown leaked, outlined in amber, and stays to the end, since it's still allocated.
Program endslists what leaked - A block whose values are too long to sit side by side, like a
Vecof structs, gets a row per element, with its index
Static memory and pointers:
- String literals and
staticitems with a literal value are drawn in static memory from the first step, since they're part of the program itself - A reference or raw pointer is an arrow. A struct holding a
&strborrows its text; one holding a raw pointer, like a hand-written smart pointer, owns its block - A write through a pointer names it:
*raw = 99reads as a write throughrawinto the variable or block it points at. A pointer into freed memory is marked dangling; a null one readsnull - A pointer stored inside a block, like a linked-list node's
next, shows the address it leads to - When several pointers lead to the same block, their arrows fan in side by side instead of crossing
Unsafe code:
- Each statement in an
unsafe { … }block is a step of its own, so*p = 10;and*p += 1;are two writes, and a Box rebuilt withBox::from_rawis freed at the block's closing brace - Raw pointers into the stack or the heap,
ptr::read,ptr::writeandptr::swap,slice::from_raw_parts,Vec::from_raw_partsandManuallyDropare all drawn as they happen. Aptr::writeinto a Vec's spare capacity shows onceset_lencounts it - Undefined behavior ends the walkthrough with a step saying which kind it was: a use after free, a double free, a read past the end of a block (with the offset and the block's size), or a pointer into a function that had already returned
- A panic gets a step of its own on the line that panicked, with Rust's message (
attempt to subtract with overflow). Each function the panic passes through unwinds, dropping what it owns, and the program ends with exit code 101 - A program that starts a thread is shown up to the first
thread::spawn, and its last step says why it stops there
1fn main() {2 let kept = Box::new(String::from("never freed"));3 let raw = Box::into_raw(kept);4 println!("{}", unsafe { &*raw });5}raw back into a Box, so when it goes out of scope the Box and its String leak. The walkthrough keeps both, outlined in amberA big program stays readable: the diagram fills the width of the panel and scrolls down, and each step is only as tall as what it shows. The zoom button in the toolbar (Fit the whole diagram) shows all of it at once, and again goes back
What it adds to your program
Aquascope has a few gaps, so the Playground quietly changes a copy of your program before running it, and undoes those changes in the drawing. Your code in the editor isn't touched, and no line moves
- Printed output. Miri throws away what a program prints, so
println!and friends are swapped for versions that pass the text along. That's how the terminal fills in - String literals. A local holding a
&'static strcrashes Aquascope, solet s = "hi";, or a literal passed to a function likecreate_item("Apple", 100), gets a hidden owner and is drawn back in static memory - Derived
Debug. Printing a#[derive(Debug)]type with{:?}would crash it too, so the same text is built withformat!instead, for the type and for theVec,Option,Resultor tuple it sits in. The output is byte-for-byte whatrustcprints - Panics. The engine ends a panicking run as if nothing went wrong, so
mainruns under a hiddencatch_unwindthat passes the message on. That's how the walkthrough can show the line that panicked, and what it said - Struct layout. The engine works out which field a reference points into by adding up field sizes in the order they're declared, while rustc moves fields around and pads them. So a struct's fields are declared largest first and marked
#[repr(C)]: same size, but laid out as declared. The diagram still lists them in the order you wrote them - Blocks. The engine pauses only at a function's own statements, so an
unsafe { … }block would be one step. Each statement inside becomes a statement of the function, with its ownunsafe, and at the closing brace the block's variables are dropped, latest first, just as the block would. A block that needs to stay whole for the program to mean the same thing stays whole
1fn test_stack() {2 let on_stack = 6;3 println!("{} at {:p}", on_stack, &on_stack);4}56fn main() {7 test_stack();8}main. The terminal shows the real output, including the address {:p} printsHow it's tested
Every change to the Playground runs over a hundred programs through the real engine: the Rektoff lectures, everyday Rust (clones, borrows, slices, structs and enums that own heap data, Rc<RefCell>, HashMap, recursion, Drop order, closures), unsafe Rust with real undefined behavior, and low-level code of the kind Solana programs are made of (byte serialization, account handles, zero-copy casts, overflow, missing owner and signer checks). Each must print exactly what a native rustc build prints, and its walkthrough must keep the heap consistent: a freed block never comes back, two live blocks never share an address, a block keeps its address unless it grows, and every arrow points at something drawn
They're also run through full Miri, which checks Rust's aliasing rules too. The Playground and full Miri must agree on every program: undefined behavior in one exactly when in the other, and a panic in one exactly when in the other