Playground
What the Playground can't do yet
The programs the engine can't step through, why, and what to write instead
In short
The Playground runs a teaching-sized subset of Rust: one file, the standard library only, no crates. When a program falls outside that, it says so on the right, naming what stopped it and what to try instead, rather than drawing something wrong
What works
Most single-file Rust a learner writes. Before the gaps, here's what the Playground steps through and draws, each checked against a native rustc build:
- Ownership: moves,
clone(), functions taking and returning owned values, shadowing, reassigning a moved variable,Dropimpls and the order they run in - Borrowing:
&and&mut, references held in structs, slices of aVecorString, methods taking&self,&mut selfandself, closures that borrow - The heap:
Box,String,Vec(growth,shrink_to_fit,remove,insert,retain,drain,split_off,clear), nested collections, aBoxlinked list,HashMapandHashSet,mem::take,mem::replaceandmem::swap - Shared ownership:
RcandArcwith their counts,Weakin one direction,CellandRefCell - Static memory: string literals and
staticitems, from the first step - Unsafe Rust: raw pointers into the stack and the heap,
Box::into_rawandfrom_raw,ptr::read,write,swapandcopy_nonoverlapping,slice::from_raw_partsandfrom_raw_parts_mut,Vec::from_raw_partsandString::from_raw_parts,ManuallyDrop,NonNull,std::alloc,transmute,read_unaligned,unsafe fn, and leaks - Byte-level code:
to_le_bytesandfrom_le_bytes,copy_from_slice,split_at_mut,try_into, account-style data in aVec<u8>, zero-copy casts to a#[repr(C)]struct,size_ofandalign_of, arrays like[u8; 32] - Arithmetic:
checked_*,wrapping_*andsaturating_*, truncatingascasts,try_from, and overflow, which panics as it does in a debug build - Panics: overflow, an index out of bounds,
unwraponNone, a secondborrow_mut,panic!. The walkthrough shows the line that panicked with its message, then the stack unwinding and dropping what it owned - Undefined behavior Miri catches: use after free, double free, reads past the end of a block, pointers into a function that already returned, a raw pointer kept across a
Vecreallocation - Everyday code: structs, enums,
OptionandResult, generics, traits withoutdyn, recursion, loops,let … else,?, closures,macro_rules!,format!, derivedDebug,CloneandPartialEq, and the code before athread::spawn {:?}on your own types, and on theVec,Option,Result,Boxand tuples they sit in
Programs that won't run
Types the engine can't read yet:
- Trait objects. A
dyn Traitvalue can't be read. Use the concrete type, or a generic - Slices of arrays, like
&arr[..], a&[i32]local, or aBox<[i32]>frominto_boxed_slice. Slices of aVecwork, so keep the numbers in aVec MaybeUninit, initialized or not. Use a plain value, or anOptionwhile it isn't set- A reference into the data of an enum variant:
if let Some(x) = &opt,match &ix { Ix::Deposit { amount } => … }, a&selfmethod that matches its own variants, or==between values inside anOptionorResultof your own type. The engine can't place such a reference yet. A reference to the whole enum works, and so does anOptionof aBox,StringorVec. Match by value instead: on the enum, a copy, or a clone - Unions. Reinterpret bits with
to_bitsandfrom_bits,to_le_bytes, ortransmute
Patterns that crash it:
- Values that point at each other in a cycle, through
RcorWeak: a child that knows its parent, say. The engine follows the pointers round and round. Links in one direction work - Some string literals.
let name = "text";and passing one to a function work. A literal stored in a struct field, in avec![…], used in amatcharm, or returned from a function doesn't. Use aStringthere - Hand-written
DebugorDisplayimpls. Any function with a namedFormattercrashes the engine.#[derive(Debug)]works; afmtyou write yourself doesn't - A misaligned raw pointer read, like a
u64read at an odd offset of a byte buffer. Rust's debug checks abort the program on the spot, before there's anything to draw, and the Playground says so. Useptr::read_unaligned, orfrom_le_bytes - Loops that never end. A run is stopped after 20 seconds
1// Instead of a slice of an array…2let arr = [1, 2, 3];3let s: &[i32] = &arr;45// …slice a Vec6let numbers = vec![1, 2, 3];7let first_two = &numbers[..2];Programs that stop early
- Threads. The engine follows one thread at a time, so the walkthrough runs up to the first
thread::spawnand stops there, with a step saying why. Everything before it is shown as it ran. Code that waits for its threads, likethread::scope, stops the engine itself, so it shows a note instead: run each thread's code inmain, one after the other - Undefined behavior. Miri stops the program at the first one it sees, and the last step says which kind it was
- Panics. The walkthrough runs to the line that panicked, says so with Rust's message, and shows the stack unwinding. The program then exits with an error, as it would
- Pointers to unaligned data, like
addr_of!on a field of a#[repr(packed)]struct. Reading it withread_unalignedis fine, but the engine can't follow such a pointer, so the walkthrough stops there and says why
Drawn simpler than real memory
- Addresses.
{:p}prints addresses from Miri's simulated memory. The addresses in the diagram are made up to read easily. Both are consistent on their own, but they don't match each other - Loops. The engine pauses only at the statements of the function itself, so a whole
fororwhileloop, and everything inside it, shows as one step with its final values. The same goes for anifand for a block inside a loop - Some blocks. An
unsafe { … }or{ … }block directly in a function is split into a step per statement. One that declares a name also used outside it (shadowing, say), defines items inside, or hands back a value stays one step - Statics. Only
staticitems with a literal value are drawn, andstatic mutisn't: the engine doesn't trace statics, so their changes can't be shown - HashMaps. A
HashMaporHashSetshows how many entries it holds and its table on the heap, but not the keys and values inside: to the engine, the table is raw bytes - Leaks. The engine only sees heap blocks the program can still reach, so a freed block and a leaked one both just vanish from its trace. The Playground works out which from what pointed at the block last: an owner means freed; only a raw pointer or a reference, or
mem::forget, means leaked - std's machinery. Internals are drawn by what they are: a
RefCellas its value, an atomic as its number, a thread'sJoinHandleandThreadby name - Other output. Only
println!,print!,eprintln!andeprint!reach the terminal.dbg!and writing toio::stdout()directly don't - Aliasing bugs. This build of Miri doesn't check Rust's aliasing rules (Stacked Borrows). Writing through a raw pointer after taking a
&mutto the same data isn't flagged, though full Miri would flag it. The memory drawn is still right. Every unsafe example in these docs and the Playground's tests is also checked with full Miri - Panic locations. The terminal shows
thread 'main' panicked at line 5:where a real run gives the file and column,src/main.rs:5:13:
Why these limits
Most of them are gaps in Aquascope itself, which is still a research project. It finds the place a reference points to by walking the type's layout, and that walk doesn't handle enum variants or slices of arrays yet. The Playground works around what it safely can (printing, string literals, derived Debug, leaks, threads, blocks, panics, struct layout) and names the rest clearly. As Aquascope improves, the engine can be updated and some of these will go away