{"id":50947105,"url":"https://github.com/root-11/intro-book","last_synced_at":"2026-06-17T21:05:13.296Z","repository":{"id":357009004,"uuid":"1234160795","full_name":"root-11/intro-book","owner":"root-11","description":null,"archived":false,"fork":false,"pushed_at":"2026-06-01T19:03:45.000Z","size":19429,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"gh-pages","last_synced_at":"2026-06-01T20:23:50.009Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/root-11.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-09T20:34:45.000Z","updated_at":"2026-06-01T19:04:40.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/root-11/intro-book","commit_stats":null,"previous_names":["root-11/intro-book"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/root-11/intro-book","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/root-11%2Fintro-book","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/root-11%2Fintro-book/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/root-11%2Fintro-book/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/root-11%2Fintro-book/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/root-11","download_url":"https://codeload.github.com/root-11/intro-book/tar.gz/refs/heads/gh-pages","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/root-11%2Fintro-book/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34465388,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-17T02:00:05.408Z","response_time":127,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-17T21:05:13.039Z","updated_at":"2026-06-17T21:05:13.287Z","avatar_url":"https://github.com/root-11.png","language":"HTML","funding_links":[],"categories":[],"sub_categories":[],"readme":"# An Introduction to Programming, using ECS \u0026 EBP in Rust\n\n**Read it online: https://root-11.codeberg.page/intro-book/**\n\nA book that teaches programming from first principles of data-oriented design, entity-component-systems (ECS), and existence-based processing (EBP). No prior programming experience assumed.\n\nThe through-line is a small ecosystem simulator built in stages from one hundred wandering creatures to a hundred million streamed ones. Forty-three sections; two-to-three pages of prose plus four-to-twelve compounding exercises each.\n\nFound something wrong, unclear, or worth adding? [Open an issue.](https://codeberg.org/root-11/intro-book/issues)\n\n\u003c!-- BOOK_BEGIN --\u003e\n\n\u003c!-- This block is generated by build.py - do not edit by hand. --\u003e\n\n_written by [Bjorn Madsen](mailto:drbjornmadsen@gmail.com)_\n_updated: 2026-06-06_\n\n\u003e **Read online:** [Codeberg](https://root-11.codeberg.page/intro-book/) · [GitHub Pages](https://root-11.github.io/intro-book/)\n\u003e\n\u003e **Clone source** (the public default branch is the rendered book; the runnable code lives on `main`): `git clone --branch main https://codeberg.org/root-11/intro-book.git` · `git clone --branch main https://github.com/root-11/intro-book.git`\n\u003e\n\u003e **Issues:** [Codeberg](https://codeberg.org/root-11/intro-book/issues) · [GitHub](https://github.com/root-11/intro-book/issues)\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"illustrations/classroom.jpg\" alt=\"A classroom: Understand, Model, Solve, Validate, Improve\" style=\"max-height: 360px; max-width: 100%;\"\u003e\u003c/p\u003e\n\nThis book teaches programming from first principles of data-oriented design, entity-component-systems (ECS), and existence-based processing (EBP). It assumes no prior programming experience and uses Rust as the only language.\n\nThe book is structured around forty-three concepts ([the DAG](https://root-11.codeberg.page/intro-book/concepts/dag.html)) and their canonical wording ([the glossary](https://root-11.codeberg.page/intro-book/concepts/glossary.html)). Sections are short - two to three pages of prose followed by four to twelve compounding exercises. Concepts are *named* only after they are *built*: every section earns its vocabulary through working code, not the other way around.\n\nThe through-line is a small ecosystem simulator built in stages from one hundred wandering creatures to a hundred million streamed ones. The simulator's specification is at [`code/sim/SPEC.md`](https://root-11.codeberg.page/intro-book/code/sim/SPEC.html).\n\nThis is a work in progress. Section ordering is by the DAG; reading order can be linear (front to back) or by following the cross-links wherever they lead.\n\n## Who this book is for\n\nYou want to build something. You are either coming to programming from another field, or you tried it before and found that what got taught did not match what you wanted to make. You can read code; you may have written some; you have not been bitten enough to feel that programming is *yours* yet.\n\nThe book is for people who learn by building artifacts and want technical depth that *compounds* - where each new idea makes the previous one more useful, not just adds another tool to a pile. The through-line is a small ecosystem simulator that grows from a hundred creatures to a hundred million; everything you learn earns its keep on that one program, then transfers everywhere else.\n\nIt is not aimed at the median CRUD-application job market. If your goal is \"any programming job, fastest,\" there are faster paths. If your goal is \"the kind of programmer whose programs work,\" this is one of them.\n\n## Background\n\nYou should be comfortable with high-school algebra and a command line - running a command, changing directories, reading error messages without panic. A laptop with internet is enough for the first ten sections; for the rest, you will install a Rust toolchain locally.\n\nYou do *not* need prior programming experience, calculus, a maths degree, or any prior contact with Rust. The book teaches Rust syntax as each section needs it; the language is a vehicle, not the subject.\n\n## A first taste\n\nBefore any vocabulary is named, here is what an ECS world looks like in fifteen lines of Rust. One hundred creatures, each with a position and a velocity, moving for thirty ticks of simulated time. No structs, no traits, no libraries - four `Vec`s indexed in lockstep, and a function (the `for i in 0..x.len()` loop) that advances every creature one step.\n\n```rust,editable\nfn main() {\n    let mut x:  Vec\u003cf32\u003e = (0..100).map(|i| (i as f32) * 0.1).collect();\n    let mut y:  Vec\u003cf32\u003e = (0..100).map(|i| (i as f32).sin()).collect();\n    let     vx: Vec\u003cf32\u003e = (0..100).map(|i| ((i * 7) % 11) as f32 * 0.01 - 0.05).collect();\n    let     vy: Vec\u003cf32\u003e = (0..100).map(|i| ((i * 13) % 7) as f32 * 0.01 - 0.03).collect();\n\n    for tick in 0..30 {\n        for i in 0..x.len() {\n            x[i] += vx[i];\n            y[i] += vy[i];\n        }\n        if tick % 10 == 0 {\n            println!(\"tick {tick}: creature 17 at ({:.2}, {:.2})\", x[17], y[17]);\n        }\n    }\n}\n```\n\nClick play. The simulator runs in your browser, prints three lines, and stops. That is the entire shape of what the rest of the book grows: tables (the `Vec`s), a tick (the outer loop), a system (the inner loop). Everything that follows is the discipline that lets this same shape carry a hundred million creatures without falling apart.\n\n## Running the code\n\nMost code blocks in the early chapters have a play button that runs the code in your browser via the [Rust Playground](https://play.rust-lang.org). Click it, edit, see the result. No setup required. The deck-game exercises in §5, §9, and §10 are designed to be run this way - open the page, hit play, work through the exercises in the editor that appears.\n\nFrom the simulator chapters onward, the exercises stop being self-contained snippets. They build the through-line: a working Rust program that grows from one hundred wandering creatures to a hundred million streamed ones. Running them needs a local Rust toolchain, a project that holds state between runs, and the ability to time loops on your own hardware. By that point you will want a clone of the book's repo:\n\n```sh\ngit clone https://codeberg.org/root-11/intro-book.git\ncd intro-book\ncargo run --release --bin sim\n```\n\nFor the timing exercises in §1, the play button works but the numbers it produces are not yours - they come from a shared server the playground happens to be running on. The exercise asks \"how fast does *your* machine run this?\", and that question only has a real answer locally. Click play for a first taste; then run on your own hardware for the numbers the rest of the book references.\n\nThe threshold between *playground* and *local* is fuzzy by intent. A reader on a phone or in a classroom can stay in the browser through §10. Beyond that, treat a local toolchain as part of the curriculum.\n\n## The companion edition\n\nIf you want to read the same book in a slow language and see what *discipline* must replace what the type system here enforces for you, the [Python edition](https://root-11.codeberg.page/intro-book-python/) covers the same forty-four sections in Python and `numpy`. The architecture is identical; the language differs. Many readers find Python a useful contrast: every borrow-check error here is a runtime mistake there, and the per-chapter Python commentary names the cost.\n\n\n# Nomenclature\n\nQuick reference for symbols, notation, and abbreviations the book uses. Concept *definitions* live in the [glossary](https://root-11.codeberg.page/intro-book/concepts/glossary.html); this page covers the shorthand only.\n\n## Symbols\n\n| Symbol | Meaning |\n|---|---|\n| §N | Section number - e.g., §5 refers to section 5. |\n| → | Leads to / becomes / transitions to. Appears in section titles (e.g., §29 \"10K → 1M\") and prose. |\n| `[!NOTE]` / `[!TIP]` / `[!WARNING]` | Callout box - content the reader should pay particular attention to. |\n\n## Text formatting\n\n| Form | Meaning |\n|---|---|\n| `monospace` | Code: types, variable names, function names, file paths. |\n| *italic* | First definition of a term, or emphasis. |\n| **bold** | A term being highlighted as load-bearing in the current paragraph. |\n\n## Variables you will see across chapters\n\n| Variable | Meaning |\n|---|---|\n| `i`, `j` | Index into a table. `i` is the index of the row currently under discussion. |\n| `t` or `tick` | Tick number - the simulator's step counter. |\n| `id` | Stable entity identifier (an integer). |\n| `gen` | Generation counter, paired with a slot index to detect stale references (§10). |\n| `pos`, `vel` | Position and velocity of a creature. |\n| `to_remove`, `to_insert` | Buffers of pending mutations applied at end-of-tick (§22). |\n\n## Rust types used in code\n\n| Type | What it is |\n|---|---|\n| `Vec\u003cT\u003e` | Heap-allocated, growable array of `T`. The book's \"table.\" |\n| `\u0026[T]` | Read-only borrow of a contiguous slice. |\n| `\u0026mut [T]` | Mutable borrow of a contiguous slice. |\n| `usize` | Pointer-sized unsigned integer. Used for table indices. |\n| `u8` / `u16` / `u32` / `u64` | Unsigned integers, sized in bits. |\n| `f32` / `f64` | 32-bit and 64-bit floats. |\n\n## Abbreviations\n\n| Acronym | Expanded |\n|---|---|\n| ECS | Entity-Component-Systems |\n| EBP | Existence-Based Processing |\n| DOD | Data-Oriented Design |\n| SoA | Structure of Arrays - each field is its own column. |\n| AoS | Array of Structures - each row is its own struct. |\n| DAG | Directed Acyclic Graph |\n| IOPS | I/O Operations Per Second |\n| TDD | Test-Driven Development |\n| LRU | Least Recently Used (cache eviction policy) |\n\n*EBP* is this book's shorthand. The spelled-out term - *existence-based processing* - is Richard Fabian's, from [*Data-Oriented Design*](https://www.dataorienteddesign.com/dodbook/); §17 introduces it from the simulator. An acronym index will not list \"EBP\" because the source literature spells the term out rather than abbreviating it.\n\n## Naming in code\n\n- `snake_case` for variables, functions, fields.\n- `PascalCase` for types and traits.\n- `SCREAMING_SNAKE` for constants.\n- File names mirror their dominant content: `creatures.rs` defines the creature table, `motion.rs` the motion system.\n\n\n# 1 - The machine model\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"covers/phase_foundation.jpg\" alt=\"Foundation phase\" style=\"max-height: 380px; max-width: 100%;\"\u003e\u003c/p\u003e\n\nMost explanations of \"how a computer works\" use a diagram with a CPU and a single big block called *memory*. The diagram is wrong. Memory is many things at different speeds, and which one your data sits in decides whether your program is fast or slow.\n\nInside the CPU there is **L1 cache** - small, sometimes only 32 KB per core, but a read from it costs about one nanosecond. Around it sits **L2** - a few hundred KB, around 3-4 ns. Then **L3** - measured in megabytes, around 10 ns. Outside the CPU sits **main memory (RAM)** - gigabytes, around 100 ns per read. The numbers vary by chip, but the *ratios* are stable: L1 is roughly a hundred times faster than RAM. Cache and RAM are the same kind of thing - bytes that the CPU reads - but they sit at very different distances from the arithmetic units.\n\nWhen your code reads `vec[17]`, the CPU does not pull just byte 17. It pulls a whole 64-byte chunk - a *cache line* - and keeps that line in L1. The next read of `vec[18]` is then almost free. Reading sequentially through a `Vec` is fast because every line that gets loaded is mostly used before it gets evicted. Reading at random is slow because every read costs a fresh trip to RAM.\n\nA pointer is an address in memory. Following one - `*ptr` - is one memory read at an address the CPU does not get to predict. If the address is in cache, the read is fast; if not, you wait the full ~100 ns. A program with many objects and many pointers between them is a program with many of those waits.\n\nThat asymmetry is the dominant fact about modern CPUs. The arithmetic - adding, multiplying, branching - is virtually free; the cost is *getting the data to the arithmetic*. A program that respects this is fast. A program that ignores it can be a hundred times slower\u003csup\u003e2\u003c/sup\u003e than a program that does the same work, with the same number of additions, but in a layout the cache likes.\n\nThis is also what makes \"complexity class\" misleading on its own. An O(N log N) algorithm that hits the cache hard can outrun a \"faster\" O(N) algorithm that scatters reads across RAM. Big-O describes how cost grows with N; layout describes the constant factor that gets multiplied in. At the scales this book targets, the constant factor often wins.\n\nYou will *measure* this in the next two sections. The numbers above are nominal - the chip in front of you may be slightly faster or slightly slower, and the ratios are what matters. Once you have felt how big the gap is, the rest of the book's reasoning about layout, SoA, locality, and parallelism follows naturally.\n\n## Measurements\n\nReference values for these calibrations - yours will differ by machine, and the spread is the point. Full per-machine output: `code/README.md`.\n\n| # | measurement | Ryzen 9 (modern) | i7-3610QM (2012) | i3-5010U (2015) | Pi 4 |\n|---|---|---|---|---|---|\n| 1 | Vec sum, ns/element at N = 100M | 0.14 | 0.44 | 0.70 | 2.03 |\n| 2 | pointer-chase vs Vec sum, 1M (random ÷ sequential) | 270x | 120x | 103x | 63x |\n| 3 | cache cliffs visible in the ns/element staircase | 1 (L3→RAM) | 3 | 2-3 | 3 |\n\n## Exercises\n\nThese exercises are calibrations. Run them on your machine and write the numbers down - the rest of the book references them.\n\n1. **Look up your cache sizes.** On Linux, `lscpu | grep -i cache` lists L1d, L1i, L2, L3 per core. Write them down. (On macOS: `sysctl -a | grep cache`.) These are the budgets node 25 will hold you to later.\n2. **Time a sequential sum.** Build a `Vec\u003cu64\u003e` of 100,000,000 elements (use `vec![1u64; 100_000_000]`), then time `vec.iter().sum::\u003cu64\u003e()`. Use `std::time::Instant`. Note the time per element in nanoseconds.\n3. **Time a random-access sum.** Build the same `Vec\u003cu64\u003e`, plus a `Vec\u003cusize\u003e` of 100,000,000 random indices. Time the loop `let mut s = 0u64; for \u0026i in \u0026indices { s += vec[i]; }`. Compare with exercise 2.\n4. **Find the cache cliffs.** Repeat exercise 2 at sizes 1K, 10K, 100K, 1M, 10M, 100M. Plot `time/element` (or just print it). Note the size at which it jumps - that's where you spilled out of L1, then L2, then L3.\n\n\u003e [!NOTE]\n\u003e What you see depends on your CPU. On older or smaller chips (Raspberry Pi 4 Cortex-A72, 2012 i7-3610QM, 2015 i3-5010U), the L1, L2, and L3 transitions appear as a graded staircase in ns/element. On modern desktop chips, a stronger prefetcher and wider SIMD often merge L1/L2/L3 into a single visible cliff at the L3→RAM boundary. Both are correct - both teach the same point. If you see one cliff on your machine, repeat the exercise with random indices (the §1.3 pattern) to surface the others.\n\n5. **Pointer chasing.** Build a linked list of 1,000,000 `Box\u003cNode\u003e` where `Node { value: u64, next: Option\u003cBox\u003cNode\u003e\u003e }`. Time a sum that walks the list. Compare with the same sum on a `Vec\u003cu64\u003e` of the same length. The ratio is roughly the L1-to-RAM ratio.\n\n\u003e [!NOTE]\n\u003e The ratio depends on your CPU. Measured: ~60× on a Raspberry Pi 4, ~90-115× on mid-2010s Intel laptops, ~270× on a Ryzen 9 270. The wider gap on newer hardware reflects faster cores running ahead of an unchanged DRAM latency. The order of magnitude (60-270×) is robust; the exact factor is not. Note also: a list built by `for i in (0..N).rev() { Box::new(...) }` allocates the boxes at *sequential* heap addresses - the chase looks free. Shuffle the order in which you thread them to surface the real cost.\n\n6. *(stretch)* **Read your `lscpu` output to your benchmarks.** With your cache sizes from exercise 1 and your timings from exercise 4, identify which level of cache each size step is leaving. The transitions are not always clean - annotate where they are noisy.\n\nReference notes for these exercises in [01_the_machine_model_solutions.md](https://root-11.codeberg.page/intro-book/trunk/01_the_machine_model_solutions.html).\n\n## What's next\n\nThe numbers you wrote down in exercise 1 and the cliffs you found in exercise 4 are the constants behind the whole book. [§2 - Numbers and how they fit](#2---numbers-and-how-they-fit) takes the next step: how big is each unit of data, and how many fit in a cache line?\n\n\n# 2 - Numbers and how they fit\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"illustrations/multimeter.jpg\" alt=\"A mouse with a multimeter - numbers measured to the precision the budget allows\" style=\"max-height: 300px; max-width: 100%;\"\u003e\u003c/p\u003e\n\nA cache line is 64 bytes on x86 and most ARM chips - the unit of memory the CPU loads at a time. (A few designs differ: some Apple Silicon cache levels use 128; §33 has the details.) This book assumes 64 throughout. Everything you do with data is, in part, a question of how many things fit in one cache line.\n\nRust gives you several integer widths: `u8` (one byte, 0 to 255), `u16` (two bytes, 0 to 65 535), `u32` (four bytes, around four billion), `u64` (eight bytes, around 1.8×10¹⁹). The signed versions - `i8`, `i16`, `i32`, `i64` - use one bit for the sign and the rest for magnitude. For floating-point: `f32` (four bytes, ~7 decimal digits of precision), `f64` (eight bytes, ~15 decimal digits).\n\nA `Vec\u003cu8\u003e` of length N is N bytes. A `Vec\u003cu64\u003e` is 8N bytes. So a `Vec\u003cu8\u003e` fits 64 elements per cache line; a `Vec\u003cu64\u003e` fits 8. Walk the whole vector and the `u64` version pulls in 8× as many cache lines as the `u8` version: the same element count, eight times the bytes\u003csup\u003e1\u003c/sup\u003e.\n\nThis is the *width budget*. Picking a wider type than you need is not free; it costs cache lines, and at the scales this book targets, cache lines are the budget you spend.\n\nThe rule is simple: pick the narrowest type that holds your range, and write down why. A 52-card deck's `suits` need 4 values, `ranks` need 13, `locations` need maybe 8 - all fit in `u8`. A creature's `pos` needs about ten kilometres of grid resolved to centimetre precision; that fits in `f32`. A timestamp in microseconds for a year-long simulation needs something like 3×10¹³, which does not fit in `u32` (4×10⁹) but fits comfortably in `u64`. Choose, and write the choice down.\n\nFloats are the trickier case. They look like real numbers but are not. There are only about 4 billion `f32` values; there are only about 18 quintillion `f64` values; that is finite. Operations have edges: `1.0 / 0.0 = inf`, `0.0 / 0.0 = NaN`, and `NaN != NaN` - yes, equality is broken on purpose, because there is no reasonable answer. Subtracting two nearly equal floats loses most of their precision (this is *catastrophic cancellation*). Adding a tiny float to a large one quietly drops the tiny one (this is *absorption*). None of this is a problem if you know it is there; all of it is a problem if you assume floats are mathematics.\n\nMost of this book uses `u8`, `u16`, `u32`, `f32`, and `u64` for time. `i*` and `f64` appear when the range or precision genuinely demands it. The choice is documented at every column declaration.\n\n## Measurements\n\nEight times the bytes is *less* than eight times the time - the sum is bandwidth-bound, not purely line-count-bound, and a wider type also feeds the prefetcher more to chew on. Full output: `code/README.md`.\n\n| # | measurement | Ryzen 9 (modern) | i7-3610QM (2012) | i3-5010U (2015) | Pi 4 |\n|---|---|---|---|---|---|\n| 1 | u8 vs u64 sum, N = 100M | 1.8x | 2.0x | 2.5x | 4.6x |\n\n## Exercises\n\n1. **Sizes.** Print `std::mem::size_of::\u003cu8\u003e()`, `\u003cu16\u003e`, `\u003cu32\u003e`, `\u003cu64\u003e`, `\u003ci32\u003e`, `\u003cf32\u003e`, `\u003cf64\u003e`, `\u003cusize\u003e`. Confirm `usize` is 8 on a 64-bit machine.\n2. **Cache-line packing.** For each type above, compute how many fit in a 64-byte cache line. A `Vec\u003cu32\u003e` of 16 elements is exactly one line; a `Vec\u003cu64\u003e` of 8 elements is exactly one line.\n3. **Width and speed.** Sum a `Vec\u003cu8\u003e` of 100,000,000 ones, then a `Vec\u003cu64\u003e` of the same length. Compare times. Some of the difference is memory bandwidth (8× more bytes); some is cache pressure.\n4. **Float weirdness.** Compute `0.0_f64 / 0.0_f64`, `1.0_f64 / 0.0_f64`, and `(0.0_f64).sqrt()`. Print them. Then check `let nan = 0.0_f64 / 0.0_f64; assert!(nan != nan);` - confirm it does not panic.\n5. **Catastrophic cancellation.** Compute `1e10_f32 - (1e10_f32 - 1.0_f32)`. The result should be `1.0`; on `f32` it usually is not. Repeat with `f64` and observe it gets closer.\n6. **Choose a width.** For each of these columns, write down the type you would pick and why: a creature's age in ticks at 30 Hz over a year-long simulation; a card's suit; the pixel count of a 4K screen; the user id in a system with up to 100 million users; an audio sample value in 16-bit PCM.\n7. *(stretch)* **The actual range of `f32`.** Read [the `f32` documentation](https://doc.rust-lang.org/std/primitive.f32.html). What is `f32::MAX`? `f32::EPSILON`? What does the latter mean for a sum of small numbers?\n\n## What's next\n\n[§3 - The `Vec` is a table](#3---the-vec-is-a-table) takes the next step: now that you know how big the elements are, what does a `Vec\u003cT\u003e` *do* with them?\n\n\n# 3 - The `Vec` is a table\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"illustrations/linear_algebra.jpg\" alt=\"Linear algebra: Ax = b - a table is a matrix of columns indexed in lockstep\" style=\"max-height: 300px; max-width: 100%;\"\u003e\u003c/p\u003e\n\nA `Vec\u003cT\u003e` is three things stored on the stack: a pointer to a contiguous run of `T` values on the heap, the current length, and the current capacity. The values themselves live on the heap, side by side, with no padding between them. `vec[i]` computes `ptr + i * size_of::\u003cT\u003e()` and reads.\n\nThis is the only container the trunk of this book uses. There are no hash maps, no linked lists, no trees - not because they do not exist, but because almost every problem the book teaches is a problem of \"process all the rows of a table\", and a `Vec\u003cT\u003e` *is* the table. Adding any other container costs cache, costs allocations, and breaks the sequential-access pattern that nodes 1 and 2 just told you to want.\n\n`vec.push(x)` adds an element. If there is capacity, it writes into the next slot - O(1). If not, it allocates a larger heap region (typically twice the current capacity), copies everything across, and frees the old one. Amortised over many pushes that is O(1), but each individual push *might* be expensive. If you know how many elements you are going to insert, `Vec::with_capacity(n)` allocates once and avoids the copies.\n\n`vec.swap_remove(i)` removes the element at `i` in O(1) by moving the last element into the freed slot. Order is sacrificed for speed. This will earn its keep at [§21](#21---swap_remove).\n\n`vec.iter()` walks the slots in order. The compiler can usually turn this into a tight memory-bandwidth-bound loop with auto-vectorisation. `vec.iter_mut()` does the same, with mutation.\n\nA `\u0026[T]` is a *slice* - a pointer plus a length, without the capacity. It is what functions usually take when they want to read a `Vec` without owning it. `\u0026mut [T]` is the same with mutation. Most systems in this book have signatures like `fn motion(px: \u0026mut [f32], py: \u0026mut [f32], vx: \u0026[f32], vy: \u0026[f32])` - read these, write those, no ownership taken.\n\nThat is the full vocabulary you need from `Vec` for the next several phases. Everything else (`HashMap`, `BTreeMap`, `Box\u003cNode\u003e`, `Rc\u003cRefCell\u003cT\u003e\u003e`, `LinkedList`) is something you will reach for only when an exercise demands it and the from-scratch test (node 40) shows it earns its weight.\n\n## Measurements\n\nOrder of magnitude (60-200×) is the durable claim; the exact factor widens with the machine because the `Vec` sum vectorises and prefetches and `HashMap::get` cannot. Full output: `code/README.md`.\n\n| # | measurement | Ryzen 9 (modern) | i7-3610QM (2012) | i3-5010U (2015) | Pi 4 |\n|---|---|---|---|---|---|\n| 1 | Vec index vs HashMap get, 1M | 160x | 89x | 77x | 65x |\n\n## Exercises\n\n1. **Layout.** Print `std::mem::size_of::\u003cVec\u003cu32\u003e\u003e()`. It should be 24 on a 64-bit machine - three pointer-sized fields. Notice that the size of the *Vec value* does not depend on how many elements it holds.\n2. **Capacity vs length.** Build `let mut v: Vec\u003cu32\u003e = Vec::new();`. In a loop from 0 to 100, print `v.len()` and `v.capacity()` after each `v.push(i)`. Observe the capacity doubling pattern: 0, 4, 8, 16, 32, 64, 128.\n3. **Pre-size.** Build `let mut v = Vec::with_capacity(100);` and push 100 elements. Print `len` and `capacity` once at the end. There were no reallocations.\n4. **Indexing cost.** Sum a 1M `Vec\u003cu32\u003e` by index: `black_box((0..N).map(|i| v[i] as u64).sum::\u003cu64\u003e())`. Time it. Now build a `HashMap\u003cusize, u32\u003e` with the same entries (key `i` maps to the value) and sum it *by the same indices*: `(0..N).map(|i| map[\u0026i] as u64).sum::\u003cu64\u003e()`. Each `map[\u0026i]` is one hash-and-probe, so this measures indexed lookup. Draining the map with `into_values().sum()` walks it in bucket order instead, a cheap sequential pass that measures the wrong thing. Indexed `Vec` reads should be ~10-100× faster\u003csup\u003e1\u003c/sup\u003e.\n\n\u003e [!NOTE]\n\u003e Measured ratios: ~65× on a Raspberry Pi 4, ~90-95× on mid-2010s Intel laptops, ~160× on a Ryzen 9 270. All use Rust's default `HashMap` (SipHash). Modern hardware widens the gap because the `Vec` sum is auto-vectorized and well-prefetched; `HashMap::get` cannot be either. Order-of-magnitude (60-200×) is the durable claim.\n\n5. **`swap_remove` vs `remove`.** Build a `Vec\u003cu32\u003e` of 1,000,000 elements. Time removing 100 elements from the middle with `vec.remove(500_000)` (in a loop, because each `remove` shifts roughly half the vector). Time the same with `vec.swap_remove(500_000)`. Note the orders-of-magnitude difference.\n6. **Slices in function signatures.** Write `fn sum(xs: \u0026[u32]) -\u003e u64`. Call it with `sum(\u0026v)` where `v: Vec\u003cu32\u003e`. Note that you did not have to write `\u0026v[..]` - the conversion is automatic.\n7. *(stretch)* **A from-scratch `MyVec\u003cu32\u003e`.** Implement `MyVec` with a raw pointer, length, and capacity. Implement `new`, `push`, `get`, and `Drop`. (You will use `unsafe`. Read [the Rustonomicon's `Vec` chapter](https://doc.rust-lang.org/nomicon/vec/vec.html) when stuck.) Convince yourself a `Vec\u003cT\u003e` is a few hundred lines of careful work, no magic.\n\n## What's next\n\n[§4 - Cost is layout, and you have a budget](#4---cost-is-layout---and-you-have-a-budget) is where the layout reasoning from §1 and §2 meets the per-tick clock the rest of the book runs on. After that, [§5 - Identity is an integer](#5---identity-is-an-integer) is the card game.\n\n\n# 4 - Cost is layout - and you have a budget\n\nA program runs at some *target rate*. A game runs at 30 Hz or 60 Hz; an audio loop at 48 kHz; a control loop at 1 kHz; an interactive shell at \"as fast as a human can type\". The target rate sets a *budget* - the time available for one tick of work.\n\n|     Target rate | Budget per tick |\n|----------------:|----------------:|\n|           30 Hz |          33 ms  |\n|           60 Hz |          17 ms  |\n|         1000 Hz |           1 ms  |\n|       1 000 000 |        1 µs     |\n\nEvery operation the program does in one tick spends from that budget. Operations have very different costs: the arithmetic is virtually free, an L1 read is around 1 ns, an L3 read is around 10 ns, a RAM read is around 100 ns, a disk read is around 100 µs, a network round-trip is around 100 ms. A 30 Hz program spending one disk read per tick has lost a third of its budget on one operation.\n\n\u003e [!NOTE]\n\u003e\n\u003e Three regimes are worth naming, because the rest of the book references them. A loop is **compute-bound** when its cost is dominated by arithmetic - typically when the data fits in L1 and the inner instructions are heavy (dot products, transcendentals, integer divides). It is **bandwidth-bound** when its cost is dominated by how fast the memory subsystem can deliver bytes - typically when the working set is bigger than L3 *but* the access pattern is sequential, so the prefetcher can fill lines ahead of demand. It is **latency-bound** when its cost is dominated by individual memory round-trips - typically when the access pattern is random, so the prefetcher cannot help. The three regimes have very different time budgets and very different power profiles. A sequential `Vec\u003cu64\u003e` sum on a modern desktop is bandwidth-bound at ~50 GB/s, roughly 0.15 ns per element. The same `Vec` accessed by random index is latency-bound at one full RAM round-trip per element, roughly 50-100 ns per element - three orders of magnitude slower, despite the same arithmetic. The lesson of node 4 is that complexity-class reasoning cannot tell these regimes apart, but they are the difference between a program that meets its tick budget and one that does not.\n\nThe unit of accounting is **time** - microseconds for most real-time work, nanoseconds for tight inner loops. A 30 Hz tick has 33 ms (33 000 µs) of budget; a 1 kHz tick has 1 000 µs; a 1 MHz tick has 1 µs. When a teacher asks you \"what does this function cost?\", they are asking how many microseconds it takes. A function that costs 100 µs out of a 33 000 µs budget is fine - about 0.3% of the tick. The same function in a 1 000 µs budget is 10% of the tick. The same function in a 1 µs budget does not exist; there is no room for it.\n\nCost is also *layout*. The same algorithm that costs 100 µs on a sequential `Vec` may cost 5 ms on a hash map of the same size, because the loads scatter. Two programs with the same big-O complexity can differ by an order of magnitude on the same hardware, just because of where their data sits.\n\nThis gives you a design rule. *Decide your target rate before you decide anything else.* That sets the budget. Then when you choose data structures, ask whether the resulting working set fits in cache; ask how many memory loads per row your inner loop does; ask whether any single operation in the loop dominates the budget. Most decisions become forced once the budget is named.\n\nThe reverse direction is also useful. If you find yourself wanting to *add* something to the inner loop - a database query, a HashMap lookup, an allocation - count its cost in microseconds against the budget. Often the answer is \"this single addition uses 80% of my tick\", and the right move is not to optimise it but to lift it out of the inner loop entirely.\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"illustrations/ohms_law.jpg\" alt=\"Ohm's Law: V = I·R\" style=\"max-height: 300px; max-width: 100%;\"\u003e\u003c/p\u003e\n\nThe shape of this thinking is familiar to engineers in other domains. An electrical engineer designs a circuit by counting milliamps against a current budget. A structural engineer counts kilonewtons against a load budget. The data-oriented programmer counts memory loads and microseconds against a tick budget. *Good design is measured in millivolts and microamps* - and in nanoseconds and microseconds.\n\n\u003e [!NOTE]\n\u003e\n\u003e *Time is one budget. Power is another.* Cache hits are energetically nearly free - the data is already next to the arithmetic units. Cache misses fire up the memory controller, the bus drivers, sometimes a DRAM refresh; that is where the watts go. A loop that fits in L2 spends most of its time on cheap arithmetic; a loop that pointer-chases through RAM spends most of its time *waiting*, and during the waiting the CPU drops clocks and the chip stays cool. The same SoA-and-sequential-access discipline that fits the time budget also fits a power budget. For embedded, mobile, control, and battery-powered work, power is the *primary* budget; time is downstream of it. The \"millivolts and microamps\" line above is literal, not metaphor.\n\n## Exercises\n\n1. **Pick your rates.** For each of these systems, name a plausible target rate and the resulting per-tick budget: a card game; a real-time strategy game; a market data feed; an embedded sensor controller; a web API endpoint a user is waiting for; an offline batch job that processes a billion rows.\n2. **Count an operation.** Time a single `HashMap::get` on a map of 1 000 000 entries. Note its cost in microseconds. How many can you fit in a 30 Hz tick (33 ms)? In a 1 kHz tick (1 ms)?\n3. **The layout difference.** Sum 1 000 000 `u64`s in a `Vec\u003cu64\u003e`. Sum 1 000 000 `u64`s in a `HashMap\u003cu32, u64\u003e`. Both are O(N). What is the per-element time difference (in nanoseconds)? Where did it go?\n4. **The cliff.** With your numbers from [§1 exercise 4](#exercises), pick a `Vec` size that just fits in L2 and one that just doesn't. Time a sum loop at each size. The cliff is real.\n5. **Working backwards from the budget.** You target 60 Hz; your inner loop runs over 100 000 entities; each entity touches one cache line. Estimate the cost of the loop in microseconds and compare to your 60 Hz budget (16 666 µs). Where is your headroom?\n6. **A bad design.** Construct a design that is \"obviously fast\" by big-O reasoning but blows the 30 Hz budget on a million entities. (Hint: object-graph traversal with one heap allocation per node is a classic.)\n7. **Find your CPU's TDP.** Look up your CPU's rated thermal design power on the manufacturer's spec sheet, or read it locally on Linux with `sudo dmidecode -t processor | grep -i 'power\\|TDP'`. Note the value. TDP is what the chip can dissipate sustained without thermal throttling - burst can be 1.5-2× higher for tens of seconds; sustained settles back to TDP.\n8. **Battery budget.** A typical laptop battery holds about 50 Wh. Your simulator runs at 30 Hz and draws an average of 8 W (mostly memory bandwidth on the inner loop). How many hours of simulation does a full charge buy? If a layout change pushes more loads to RAM and raises the average draw to 14 W, how many hours then? Express the cost of the layout change as a percentage of battery life.\n9. **Measure delta power.** A ready-made workload generator lives at `code/measurement/`. In one terminal: `cargo run --release --bin power_loop -- sequential` (then in a second run: `... -- random`). In another terminal, while the loop is running: `sudo perf stat -a -e power/energy-pkg/ -- sleep 30` reads the package-energy counter over 30 seconds. Run the perf command three times - idle, sequential, random - and write the joules down. Convert each to average watts. The random-access run should draw more watts than the sequential one, which should draw more than idle.\n\n   While you are there: from `power_loop`'s iteration count, compute your sequential read bandwidth - `iterations × 10⁷ × 8 / 45` gives bytes per second - and compare to the published peak of your DDR generation. If you get within a factor of two of peak, your inner loop is *bandwidth-bound* (the regime named in the prose). The `random` mode's iteration count, divided into wall time, gives your effective per-element latency in nanoseconds; that is the *latency-bound* regime.\n10. *(stretch)* **Joules per access.** Approximate energies per memory read: L1 hit ≈ 0.1 nJ, L2 ≈ 1 nJ, RAM ≈ 30 nJ (rough; published numbers vary by chip and process). Estimate the total energy of summing 10⁷ `u64`s sequentially (mostly prefetched, near-L1 cost) versus by random indices (mostly RAM misses). Convert both to milliwatt-hours and express as a fraction of a 50 Wh battery. The absolute numbers are tiny; the *ratio* is what your battery life and your data-centre electricity bill care about.\n\n## What's next\n\nYou now have the machine model (§1), the data widths (§2), the table primitive (§3), and the budget calculus (§4). The next section is the conceptual heart of the book: [§5 - Identity is an integer](#5---identity-is-an-integer). The card game is waiting.\n\n\n# 5 - Identity is an integer\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"covers/phase_identity_structure.jpg\" alt=\"Identity \u0026 structure phase\" style=\"max-height: 380px; max-width: 100%;\"\u003e\u003c/p\u003e\n\nHand a programmer fifty-two cards and tell them to write code that shuffles, sorts, and deals. Ask how long.\n\nMost will start drawing classes - `Card`, `Deck`, `Hand`, `Player`, maybe a `Game` - and quote you four hours. They are being honest. The class hierarchy is real work. There will be constructors, copy semantics, and a vague unease about whether `Hand` should hold pointers or values, whether `Deck` owns its cards or borrows them, whether shuffling should mutate the deck or return a new one.\n\nThe whole problem fits in three lines. The way it fits is the lesson of this section.\n\nA deck of cards has three pieces of information per card: its suit (♠ ♥ ♦ ♣), its rank (A, 2, ..., K), and its current location (in the deck, in someone's hand, in the discard pile). That is three columns. The deck itself is fifty-two rows.\n\nIn Rust:\n\n```rust\nlet suits:     Vec\u003cu8\u003e = vec![ /* 52 entries: 0..4 */ ];\nlet ranks:     Vec\u003cu8\u003e = vec![ /* 52 entries: 0..13 */ ];\nlet locations: Vec\u003cu8\u003e = vec![ /* 52 entries: 0=deck, 1=hand1, ... */ ];\n```\n\nThat is the deck. There is no `Card` struct. There is no `Deck` class. The card at index `17` has its suit at `suits[17]`, its rank at `ranks[17]`, and its current location at `locations[17]`. The card *is* the index.\n\nDealing a card from the deck to player 1 is one line:\n\n```rust\nlocations[17] = 1; // card 17 is now in player 1's hand\n```\n\nAsking *what's in player 1's hand* is one loop:\n\n```rust\nlet mut hand: Vec\u003cusize\u003e = Vec::new();\nfor i in 0..52 {\n    if locations[i] == 1 {\n        hand.push(i);\n    }\n}\n```\n\nAsking *how many cards are left in the deck* is one counter:\n\n```rust\nlet mut count = 0u32;\nfor i in 0..52 {\n    if locations[i] == 0 { count += 1; }\n}\n```\n\nShuffling - the move students expect to be hard - is shuffling the order of indices. `0..52` becomes `[7, 32, 1, 19, ...]`, and you read your way through the cards in that order:\n\n```rust\nlet mut order: Vec\u003cusize\u003e = (0..52).collect();\nfisher_yates(\u0026mut order, \u0026mut rng); // 5 lines, written below\n```\n\nLook at what just happened. Nothing about the cards changed. `suits[17]`, `ranks[17]`, and `locations[17]` are exactly the values they were before. The shuffle moved indices, not data.\n\nSorting works the same way. To sort by suit then rank, you sort the indices by `(suits[i], ranks[i])`:\n\n```rust\norder.sort_by_key(|\u0026i| (suits[i], ranks[i]));\n```\n\nThe cards do not move. Their identifiers are reordered.\n\nThat's the deck of cards in maybe twenty lines of Rust. It includes shuffle, sort, deal, and several queries. It is not a stylistic shortcut; it is what a deck of cards *is*. The OOP version's four hours of work was the cost of pretending a card was an object that owned its suit and rank, when actually a card is one number - an index - and its suit and rank are values stored in arrays at that index.\n\nWe call this **identity-is-an-integer**, and it is the precondition for every economy the rest of this book buys you. Persistence will work because tables are easy to serialise. Parallelism will work because indices are cheap to partition. Replay will work because a deck is just three arrays in a state. None of it works if you reach for `class Card`.\n\n\u003e [!NOTE]\n\u003e\n\u003e *The strong form, which we will return to later:* sometimes you do not even need the index. The pair `(suit, rank)` already uniquely identifies a playing card - there are only fifty-two such pairs. The index is a *surrogate key*; the pair is a *natural key*. For variable-quantity tables (creatures that come and go) you usually need a surrogate, because two creatures can be identical. For a constant-quantity 52-card deck, you do not.\n\n## Exercises\n\nThe first time through, write everything from scratch in `src/main.rs`. Resist the urge to add a `Card` struct or helper methods. Three `Vec`s.\n\n1. **Build the deck.** Write `fn new_deck() -\u003e (Vec\u003cu8\u003e, Vec\u003cu8\u003e, Vec\u003cu8\u003e)` that returns the suits, ranks, and locations for a fresh, ordered deck (all 52 in `location 0 = deck`).\n2. **Print a card.** Write `fn card_to_string(suit: u8, rank: u8) -\u003e String` that returns strings like `\"A♠\"`, `\"10♥\"`, `\"K♦\"`. Use it to print the whole deck.\n3. **Shuffle.** Write a tiny LCG random function (one-liner) and use it to implement Fisher-Yates on a `Vec\u003cusize\u003e`. Print the deck in shuffled order. Confirm by inspection that the `suits`, `ranks`, and `locations` arrays are unchanged.\n4. **Sort by suit then rank.** Sort the `order` vector so suits come out grouped, ranks ascending within each suit. Print again. Once again, the deck arrays are unchanged.\n5. **Deal a hand.** Move the first 5 cards from the deck (location 0) to player 1 (location 1). Print player 1's hand using `card_to_string`.\n6. **Hand query.** Write `fn cards_held_by(locations: \u0026[u8], player: u8) -\u003e Vec\u003cusize\u003e` returning all card indices currently held by a given player.\n7. **Count by location.** Write a function that returns counts grouped by location: how many in the deck, in each hand, in discard.\n8. **Deal four hands.** Deal 5 cards to each of players 1, 2, 3, 4. Print all four hands.\n9. *(stretch)* **Drop the index.** Rewrite `cards_held_by` to return `Vec\u003c(u8, u8)\u003e` of (suit, rank) pairs directly - no indices. What does this make easier? What does it make harder? (Hint: you cannot move the cards back to the deck without knowing which `i` they were.)\n10. *(stretch)* **The sort hazard.** While player 1 is holding indices `[3, 17, 21, 28, 41]`, sort the deck arrays themselves (not just the order) by suit. What does player 1 think they hold now? This is the bug node 9 (\"[sort breaks indices](https://root-11.codeberg.page/intro-book/concepts/dag.html)\") was written for. Don't fix it yet - observe it.\n\nReference solutions for exercises 1-3 in [05_identity_is_an_integer_solutions.md](https://root-11.codeberg.page/intro-book/trunk/05_identity_is_an_integer_solutions.html). Solutions for the rest follow the same shape.\n\n## What's next\n\nExercise 10 leaves you with a bug. The next section ([§9 - Sort breaks indices](#9---sort-breaks-indices)) is the fix; it teaches you to keep a stable id alongside the position so external references survive reordering.\n\n\n# 6 - A row is a tuple\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"illustrations/cad_bearing.jpg\" alt=\"A bearing's dimensioned drawing names every field\" style=\"max-height: 300px; max-width: 100%;\"\u003e\u003c/p\u003e\n\nIn §5 you built a deck of 52 cards as three `Vec`s. The card at index 17 is the triple `(suits[17], ranks[17], locations[17])`. Together those three values are *the row*. There is no `Card` struct. The row exists *implicitly* in the alignment: the same index, used in every column, recovers all the data about one card.\n\nThis is what we call a *row* throughout the rest of the book - a coherent set of values that belong to the same entity. In a `creature` table the row is `(pos[i], vel[i], energy[i], birth_t[i], id[i], generation[i])`. In a `food` table it is `(pos[i], value[i], id[i])`. The fields belong to the same entity by virtue of all sharing index `i`. There is no struct holding them; there is only the discipline that whatever index `i` you used to read one column, you also use to read every other column of the same table.\n\nThe cost of implicit binding is that you must *keep the indices aligned*. If you sort `ranks` without also sorting `suits` and `locations`, you corrupt the column: the rank stored at a slot is no longer the rank of the card whose suit and location sit beside it. The deck still has 52 entries in 52 slots, but the slots no longer describe coherent cards. This is not a hypothetical bug; [§9](#9---sort-breaks-indices) will produce it deliberately so you can feel the consequences. The structural fix in this book is simple: every operation that reorders any column of a table must reorder *all* columns of that table together.\n\nThe discipline that makes alignment maintainable is **single-writer-per-column**. If only one system writes to `locations`, and that system writes consistently, alignment is never violated. Multiple writers to the same column race against each other and produce inconsistent rows. This is what node 25 (one writer, many readers) enforces: each table is written by exactly one system, and a row is a tuple precisely because that one writer kept all its columns in step.\n\nA row is a tuple - assembled from columns indexed by the same entity, kept aligned by discipline rather than by any container holding it together.\n\n## Exercises\n\nThese extend your `src/main.rs` from §5.\n\n1. **Print row 17.** Write `fn row(suits: \u0026[u8], ranks: \u0026[u8], locations: \u0026[u8], i: usize) -\u003e (u8, u8, u8)`. Use it to print the suit, rank, and location of card 17.\n2. **Mishandle the alignment.** Sorting `suits` here would do nothing - the deck was built with suits already in order, so `suits.sort()` is a no-op that quietly hides the trap. Sort *only* `ranks` instead (`ranks.sort()`, no order vector), leaving `suits` and `locations` untouched. Print rows 5 and 17. Row 5 is plainly broken: its suit and location still belong to card 5, but its rank was overwritten by another card's. Row 17 looks fine, because slot 17 happens to hold rank 4 both before and after the sort. The column is corrupted either way; the surviving slot only proves that one spot-check is not a test.\n3. **Lockstep sort.** Reset the deck. Now sort all three columns *together* by rank, using an order vector (the technique from §10): build `order`, sort it by `ranks[i]`, then rebuild every column through that one order. Print rows 5 and 17 again. Whatever card now sits in a slot, its suit, rank, and location moved there together, so the row is coherent again.\n4. **Add a fourth column.** Add `let mut dealt_at: Vec\u003cu32\u003e = vec![u32::MAX; 52];` (when a card is dealt, write the current tick number into `dealt_at[i]`). Modify your lockstep sort to also reorder this column. Verify by spot-check that a row is still consistent after a sort.\n5. **The single-writer rule.** Write `fn reorder_deck(suits: \u0026mut Vec\u003cu8\u003e, ranks: \u0026mut Vec\u003cu8\u003e, locations: \u0026mut Vec\u003cu8\u003e, dealt_at: \u0026mut Vec\u003cu32\u003e, order: \u0026[usize])`. This function is the *only* one that should ever reorder any column of the deck. Document that contract in a comment above the function.\n6. *(stretch)* **When alignment is moot.** A query that uses only `(suits[i], ranks[i])` to identify a card - for instance, \"is this the Ace of Spades?\" - does not depend on `locations` or `dealt_at`. Write such a query. The natural-key view from §5's strong form means this query survives reorderings of unrelated columns; only `suits` and `ranks` need to be aligned with each other.\n\n## What's next\n\n[§7 - Structure of arrays (SoA)](#7---structure-of-arrays-soa) names the layout choice you have been making implicitly: each field its own column. The next section defends that choice against its alternative.\n\n\n# 7 - Structure of arrays (SoA)\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"illustrations/ecs_banner.jpg\" alt=\"Three mice: ENTITY, COMPONENT, SYSTEMS - naming the layout that splits an entity into component columns\" style=\"max-height: 300px; max-width: 100%;\"\u003e\u003c/p\u003e\n\nYour deck has three `Vec`s: `suits`, `ranks`, `locations`. Each field lives in its own array, indexed by entity. This layout is called *Structure of Arrays* - SoA. The opposite layout - a single `Vec\u003cCard\u003e` where each element is a struct holding all three fields - is called *Array of Structs* - AoS. They are different choices about *where the same data lives*.\n\n```rust,no_run\n// SoA: three columns, indexed in lockstep\nlet suits:     Vec\u003cu8\u003e = vec![/* 52 */];\nlet ranks:     Vec\u003cu8\u003e = vec![/* 52 */];\nlet locations: Vec\u003cu8\u003e = vec![/* 52 */];\n\n// AoS: one column of structs\nstruct Card { suit: u8, rank: u8, location: u8 }\nlet cards: Vec\u003cCard\u003e = vec![/* 52 */];\n```\n\nMost programmers reach for AoS by default because it groups \"related\" data together. The trouble is that in a real loop \"related\" is whatever the inner loop reads, not whatever the data model says belongs together. A system that counts cards in player 1's hand reads only `locations` - it does not need suits or ranks at all. With SoA, that loop reads exactly 52 bytes from `locations`. With AoS, the loop reads all three bytes of each `Card` (because they live next to each other in memory and arrive on the same cache line) and ignores two of them - three times the memory traffic for the same answer.\n\nAt 52 cards the difference is invisible. At one million creatures with six fields each, the difference is the difference between a 30 Hz simulation and a 5 Hz one\u003csup\u003e1\u003c/sup\u003e. The motion system in §1's simulator reads only `pos`, `vel`, and `energy` - three of six creature fields. With SoA it reads three sequential streams of exactly the bytes it needs. With AoS it reads all six fields of every creature, paying twice the memory bandwidth for half the data it actually wants.\n\nThis is the bandwidth-bound regime named in §4. SoA keeps the inner loop's working set small; AoS bloats it with fields the loop ignores. At cache-spilling sizes (any working set bigger than L3) the bloat becomes the dominant cost.\n\nSoA is therefore the default in this book. AoS is sometimes the right choice - for example when every system reads every field, or when N is so small the cache line is dominated by per-row overhead either way. But this is a tradeoff to *earn* by measurement, not to assume by habit. Write SoA first; switch to AoS only when a benchmark forces you to.\n\n## Measurements\n\nHow far SoA beats a padded array-of-structs grows with the cache budget: the small-cache Pi pays for every wasted byte (5.7x), a modern desktop with generous L3 mutes it (1.6x). The principle holds on every machine. Full output: `code/README.md`.\n\n| # | measurement | Ryzen 9 (modern) | i7-3610QM (2012) | i3-5010U (2015) | Pi 4 |\n|---|---|---|---|---|---|\n| 1 | SoA vs padded AoS, count loop at 10M (row 3 B → 20 B) | 1.6x | 2.4x | 1.9x | 5.7x |\n\n## Exercises\n\nYou will need a stopwatch (`std::time::Instant`) for some of these.\n\n1. **Build both layouts.** Take your §5 deck and add an AoS twin: a `Vec\u003cCard\u003e` of 52 entries, where `Card { suit: u8, rank: u8, location: u8 }`. Build both and verify they hold the same logical content.\n2. **Count cards in a player's hand, both ways.** Write `fn count_held_soa(locations: \u0026[u8], player: u8) -\u003e usize` and `fn count_held_aos(cards: \u0026[Card], player: u8) -\u003e usize`. Confirm they return the same number on the same deck.\n3. **Time the count at 10,000 entries.** Make `Vec\u003cu8\u003e` and `Vec\u003cCard\u003e` of length 10,000 (replicate the deck 192-fold, or fill arbitrarily). Time each `count_held_*` function. Note the ratio.\n4. **Scale to 1,000,000 entries.** Repeat at length 1,000,000. The SoA version reads 1 MB; the AoS version reads 3 MB (assuming `size_of::\u003cCard\u003e() == 3` plus padding). On most chips L2 fits one but not the other. Note where the cliff appears.\n5. **The wide-field case.** Extend the row with a 16-byte `nickname: [u8; 16]`. Rebuild both. Now AoS reads 19+ bytes per element while SoA still reads 1. Time the count again. The gap should widen sharply.\n\n\u003e [!NOTE]\n\u003e How sharp depends on your memory hierarchy. Measured ratios at N=10M: ~2× on machines with generous L3 (modern desktops, mid-2010s Intel laptops), ~6× on a Raspberry Pi 4 (no L3, narrow LPDDR4 channel). The principle is the same; the slope of the cliff scales with how badly the AoS row blows the cache budget.\n\n6. **A case where AoS wins.** Write a function that updates *every* field of one specific card. SoA writes to three different lines; AoS writes to one. For the case \"update every field of every card\" (rare in practice), AoS may even tie or win. Time it and discuss.\n7. *(stretch)* **A from-scratch `SoaDeck` struct.** Wrap the three (or four) columns in one struct that owns them all. Provide `fn reorder(\u0026mut self, order: \u0026[usize])` as the only public mutator. What do you gain in correctness? What do you lose in flexibility?\n\n## What's next\n\n[§8 - Where there's one, there's many](#8---where-theres-one-theres-many) is the universalising principle. The deck taught it implicitly; the next section names it.\n\n\n# 8 - Where there's one, there's many\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"illustrations/tip_simplify_full.jpg\" alt=\"Break complex problems into smaller parts - the singleton special-cased away\" style=\"max-height: 300px; max-width: 100%;\"\u003e\u003c/p\u003e\n\nCode is written for the array. A function that operates on one entity is just the special case of N = 1; it does not need its own abstraction. A card game with 52 cards is three arrays - suit, rank, location - not 52 objects. A simulation with 100 creatures is six arrays of length 100, not 100 instances of `Creature`. The plural is the primary unit; the singular is the trivial case.\n\nThe pattern is simple. Write the array version first. The singleton drops out as a one-element slice. To shuffle one card you swap two indices in the `order` vector - same as shuffling the whole deck. To find the highest-rank card in player 1's hand you scan the (small) hand vector - same shape as scanning all 52. To deal one card you write one cell in `locations` - same shape as dealing many cells.\n\nThis stands against an instinct most programmers acquire from OOP: the urge to write `card.shuffle()` or `creature.update()` and then puzzle over how to do it for many. The puzzle does not exist when you write for arrays from the start. `shuffle(\u0026mut deck)` is one function that works for any deck, including a deck of one. `update(\u0026mut creatures)` is one function that works for any population, including a population of one.\n\nA useful test: when you find yourself writing a method on a struct, ask *what does this look like over an array?* If the array version is shorter, drop the method. If the array version is the same length, keep the method as a function over a slice - `fn shuffle(deck: \u0026mut Deck)`, not `impl Deck { fn shuffle(\u0026mut self) }`. Either way, the singleton was never the right unit of code.\n\nThere is also a cost reason, though it does not bite at 52 cards. A method that runs on one entity at a time forces its caller to invoke it N times: N opaque calls the optimiser cannot fuse into a loop. A function over a slice is *one* call - the compiler sees the whole loop and can lift invariants, reorder, and vectorise it. Writing for the array keeps the work visible to the optimiser; writing for the singleton hides it. The bill for that hiding does not arrive until the simulator is walking a million rows a tick, and [§19](#19---ebp-dispatch) measures it there. At deck scale this is a reason to prefer the array form, not yet a speed you can feel.\n\n\"Where there's one, there's many\" is therefore not an architectural slogan but a daily practice. It costs nothing the first time. It costs everything the first time you forget.\n\n## Exercises\n\nThese extend the deck again. The aim is to feel the array-first pattern in your fingertips before §5 turns into the rest of the book.\n\n1. **The function over a slice.** Write `fn highest_rank_in_hand(hand: \u0026[u32], ranks: \u0026[u8]) -\u003e Option\u003cu8\u003e` returning the highest rank held in the supplied set of card ids. Use it on a 5-card hand. Then use it on a 1-card hand. Then use it on an empty hand. Same function, three N values.\n2. **Reverse the urge.** Given an OOP-style `Card::is_face_card(\u0026self) -\u003e bool`, rewrite it as `fn face_cards(ranks: \u0026[u8]) -\u003e Vec\u003cbool\u003e` - a function over the whole `ranks` array returning a parallel mask. Apply it to all 52 cards in one call.\n3. **The N = 0 case.** What does `highest_rank_in_hand` do for an empty `hand`? Should it panic, return `None`, or return some sentinel? Pick one and justify.\n4. **Predicate over a single value.** Suppose you want `is_red(suit: u8) -\u003e bool` for a single card (suits 0 and 1 are hearts/diamonds). Write the array version `fn red_mask(suits: \u0026[u8]) -\u003e Vec\u003cbool\u003e` first. Then convince yourself the singleton case is `red_mask(\u0026[suit])[0]` - the array version covers it.\n5. *(stretch)* **From a tutorial.** Find any Rust tutorial that uses a `struct Card` with methods (`new`, `is_face`, `display`, etc.). Rewrite their full card game as three (or four) `Vec`s plus free functions. Compare line counts. Compare clarity. Compare what happens when you want to query \"all face cards across the table\" - one function call versus a loop over per-card method calls.\n\n## What's next\n\nYou have closed Identity \u0026 structure. Cards behave; rows align; layouts are SoA; the singleton drops out. The next phase is *Time \u0026 passes*, starting with [§11 - The tick](#11---the-tick). The ecosystem simulator from `code/sim/SPEC.md` is about to start running.\n\n\n# 9 - Sort breaks indices\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"illustrations/bridge_clipboard.jpg\" alt=\"Engineer mouse with clipboard and F = ma - alignment is a structural property\" style=\"max-height: 300px; max-width: 100%;\"\u003e\u003c/p\u003e\n\nIn [§5 - Identity is an integer](#5---identity-is-an-integer), exercise 10 left you with a bug. Player 1 was holding the index list `[3, 17, 21, 28, 41]`. The dealer sorted the deck columns by suit. Player 1's hand was now wrong - the same indices, the same slots, but different cards.\n\nThat bug is the structural fact this section names. Sorting did not damage anything; the player's reference was never robust to begin with. An index points at a *slot*, not at a *thing*. When the slot's contents change, the index quietly changes meaning.\n\nIt is not only sorting. Any rearrangement does it: `swap_remove` (a O(1) deletion that moves the last row into the freed slot, coming in [§21](#21---swap_remove)), reshuffling for locality ([§28](#28---proximity-is-a-property-of-position)), compacting after a batch of deletions. The same index, the same array, the same line of code, now means a different card.\n\nThis is uncomfortable. In OOP you held a `Card` reference and the card stayed put because `Card` was a thing. In data-oriented code the card *is the slot*, and the slot does not have permanent meaning. The card you saved a reference to yesterday may be a different card today, if the deck has been touched.\n\nThere are two ways forward. The lazy one is to never rearrange the deck. That works for fifty-two cards, fails for ten thousand creatures, and becomes catastrophic for a million. The book is going to need rearrangements - sorting, deletion, compaction - at every scale beyond §0. So we need the other fix: a stable name that survives the slot it currently occupies.\n\nThat is what [§10 - Stable IDs and generations](#10---stable-ids-and-generations) does. This section's only job is to make the *slot vs name* distinction concrete enough that §10's solution feels inevitable rather than ceremonial.\n\n\u003e [!NOTE]\n\u003e\n\u003e *Why feel the pain first?* Because the fix in §10 is small - one extra column - and small fixes only stick if the student knows what they fix. Reading \"always store an id\" without first feeling the bug produces students who add ids cargo-culted, then drop them when the codebase looks too cluttered. Reading it after watching player 1 lose their hand produces students who never drop them.\n\n## Exercises\n\nYou should still have your `src/main.rs` from §5. These exercises extend it.\n\n1. **Reproduce the bug.** With player 1 holding `[3, 17, 21, 28, 41]`, sort the deck *columns themselves* (`suits`, `ranks`, and `locations` in lockstep) by suit. Print player 1's hand using `card_to_string`. Confirm the cards have changed.\n2. **A second rearrangement.** Instead of sorting, swap two cards' positions:\n   ```rust\n   suits.swap(3, 17);\n   ranks.swap(3, 17);\n   locations.swap(3, 17);\n   ```\n   Print player 1's hand again. Same bug shape, different cause.\n3. **A third rearrangement.** Remove the card at slot 3 with `swap_remove(3)` on each column. Print player 1's hand. Note that the cards at slots `[17, 21, 28, 41]` are unchanged but slot 3 may now hold what was previously the last card; meanwhile slot 51 has silently been deleted.\n4. **Quantify the breakage.** Write a function that takes the original `[3, 17, 21, 28, 41]` plus a freshly built deck, applies a Fisher-Yates shuffle to the deck columns themselves, and counts how many of the five references still point at the same `(suit, rank)` value. Run it 100 times. Roughly what fraction of references survive a random shuffle of the deck?\n5. **A reference that *can* survive.** Without writing any new code - on paper - describe what kind of reference would survive a shuffle. (Hint: you already know. The card's `(suit, rank)` is unique to that card. The reference that survives is the one that does not depend on the slot.)\n6. *(stretch)* **The cost of never rearranging.** Suppose you decide to *never* sort, swap, or remove from the deck columns, to avoid this bug forever. How would shuffling work? How would discarding a card work? Why does this not scale to ten thousand creatures?\n\nReference notes for these exercises in [09_sort_breaks_indices_solutions.md](https://root-11.codeberg.page/intro-book/trunk/09_sort_breaks_indices_solutions.html).\n\n## What's next\n\nExercise 5 points at the answer; exercise 6 makes the never-rearrange option look bad. The real fix is to store identity *separately from position* - an `id` column that travels with the row across rearrangements, with a generation counter on top for variable-quantity tables. [§10 - Stable IDs and generations](#10---stable-ids-and-generations) builds it.\n\n\n# 10 - Stable IDs and generations\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"illustrations/hard_hat_repeat.jpg\" alt=\"MEASURE / CALCULATE / DESIGN / BUILD / REPEAT - generations cycle on a stable handle\" style=\"max-height: 300px; max-width: 100%;\"\u003e\u003c/p\u003e\n\nIn [§9](#9---sort-breaks-indices) you watched a player's reference go stale because they were holding *slots*, not *names*. The fix is to give each row a name - a stable identifier - that travels with the row when it moves.\n\nA stable id is one extra column. For the deck:\n\n```rust\nlet mut ids: Vec\u003cu32\u003e = (0..52).collect();\n```\n\nNow every card has both a *slot* (its current index in the columns) and an *id* (its name). When you sort the columns, you reorder `ids` in lockstep:\n\n```rust\n// sort by suit, taking ids along\nlet mut order: Vec\u003cusize\u003e = (0..52).collect();\norder.sort_by_key(|\u0026i| suits[i]);\n\nlet new_suits:     Vec\u003cu8\u003e  = order.iter().map(|\u0026i| suits[i]).collect();\nlet new_ranks:     Vec\u003cu8\u003e  = order.iter().map(|\u0026i| ranks[i]).collect();\nlet new_locations: Vec\u003cu8\u003e  = order.iter().map(|\u0026i| locations[i]).collect();\nlet new_ids:       Vec\u003cu32\u003e = order.iter().map(|\u0026i| ids[i]).collect();\n```\n\nThe card with `id = 17` is still the same card - its suit, rank, and location are unchanged. It is just at a different *slot*.\n\nTo find a card by id, scan the `ids` column:\n\n```rust\nfn slot_of(ids: \u0026[u32], target: u32) -\u003e Option\u003cusize\u003e {\n    for i in 0..ids.len() {\n        if ids[i] == target {\n            return Some(i);\n        }\n    }\n    None\n}\n```\n\nThat is O(N), which is fine for a 52-card deck and slow for a million creatures. The fix - an `id_to_slot` map maintained on every rearrangement - is [§23 - Index maps](#23---index-maps). For now the linear scan is honest pedagogy.\n\n## Generations: when slots are reused\n\nThe deck is constant-quantity. Always 52 cards, never more, never less. The simple `id` column is enough.\n\nFor variable-quantity tables - creatures that are born and die, packets that arrive and are processed, sessions that come and go - slots get *reused*. A new creature is born in the slot that just held a dead one. Now imagine a player who held a reference to the dead creature: their reference points at the same slot with the same id, but the row at that location is a different creature.\n\nOne more column fixes it: a `generation` counter that increments every time a slot is recycled. A reference is now a pair `(id, generation)`. To dereference it, you check that the row's stored `generation` still matches the reference's `generation`. If it does, the reference is live. If it does not, the slot has been recycled since the reference was taken, and the dereference returns `None`.\n\n```rust\nstruct CreatureRef {\n    id:  u32,\n    generation: u32,\n}\n\nfn get(creatures: \u0026Creatures, r: CreatureRef) -\u003e Option\u003cusize\u003e {\n    let slot = creatures.id_to_slot.get(r.id as usize).copied()?;\n    if creatures.generation[slot] == r.generation {\n        Some(slot)\n    } else {\n        None\n    }\n}\n```\n\nThis is the pattern called a *generational arena*. It is the single mechanism behind every \"handle\" type in every ECS engine: Bevy's `Entity`, `slotmap::SlotMap`, C++'s `entt::registry`. They differ in details - width of the id, packing into a `u64`, generation overflow handling - but the structural idea is the same: one column for identity, one for generation, a checked dereference.\n\nThat is enough machinery for the rest of the book to lean on. Sorting now works because the id column travels with the row. Deletion now works because the generation counter rejects stale references. Append-only and recycling tables ([§24](#24---append-only-and-recycling)) are two policies on the same machinery.\n\n\u003e [!NOTE]\n\u003e\n\u003e *The strong form of [§5](#5---identity-is-an-integer) still applies.* If your row has a natural key - `(suit, rank)`, `(date, ticker)`, `(species, position)` - you do not need a surrogate id. The card-game deck can be played without ids; the reference that survives is the `(suit, rank)` pair, because the data is unique by construction. Surrogate ids and generations earn their keep when the data has no natural unique tuple - which is most of the time once you start producing rows at runtime.\n\n## Exercises\n\nThese extend the §5 deck once more, then take a step toward the simulator's variable-quantity case.\n\n1. **Add the id column.** Add `let ids: Vec\u003cu32\u003e = (0..52).collect();` to your deck. Modify your sort so it reorders `ids` along with the other columns. Verify the original ids are still there, just in a new order.\n2. **Find a card by id.** Implement `slot_of(ids: \u0026[u32], target: u32) -\u003e Option\u003cusize\u003e` as in the prose. Use it to look up the card with `id = 17` after a sort.\n3. **Resolve the §9 bug.** With player 1 holding *ids* `[3, 17, 21, 28, 41]` (not slots), sort the deck. Use `slot_of` to translate ids to slots and print the hand. Confirm the cards are unchanged.\n4. **Permutation-friendly hand query.** Rewrite `cards_held_by(locations, ids, player) -\u003e Vec\u003cu32\u003e` to return *ids*, not slots. The player now holds names. Test by sorting the deck after a deal and confirming `cards_held_by` still returns the same five cards.\n5. **A first generation counter.** Add `let mut generation: Vec\u003cu32\u003e = vec![0; 52];`. The 52-card deck does not actually recycle, but extend a small `swap_remove`-like operation: pop the last card from the deck (location 0), insert a \"fresh\" card at the freed slot, and bump that slot's `generation` by one. Take a `CreatureRef`-style `(id, generation)` reference *before* the operation. After the operation, look up the slot by id; check `generation[slot]` against the reference's `generation`. Confirm the dereference correctly reports stale.\n6. *(stretch)* **A tiny generational arena.** Outside the deck, build a `Creatures` struct with `pos: Vec\u003cf32\u003e`, `generation: Vec\u003cu32\u003e`, plus `free: Vec\u003cu32\u003e` of slots awaiting reuse. Implement `insert(pos) -\u003e (slot, generation)`, `remove(slot)`, and `get(slot, generation) -\u003e Option\u003cf32\u003e`. Convince yourself by example that stale references cannot read a fresh creature's data.\n7. *(stretch)* **Compare with `slotmap`.** Read [`slotmap::SlotMap::insert` and `get`](https://docs.rs/slotmap/latest/slotmap/). Identify which of your fields and operations correspond. What does `slotmap` add that you didn't need for the simulator? Decide consciously whether to adopt it. (This is the from-scratch-then-price-the-crate move from [§41 - Deferred abstraction](#41---deferred-abstraction) and [§42 - You can only fix what you wrote](#42---you-can-only-fix-what-you-wrote).)\n\nReference solutions for the deck exercises (1-5) in [10_stable_ids_and_generations_solutions.md](https://root-11.codeberg.page/intro-book/trunk/10_stable_ids_and_generations_solutions.html). The arena and `slotmap` exercises follow the same shape and are worth working without reference.\n\n## What's next\n\nYou now have stable references. The next thing the simulator will need is to look up a row by id in O(1) rather than O(N) - an `id_to_slot` map maintained on every reordering. That is [§23 - Index maps](#23---index-maps). It is one extra `Vec\u003cu32\u003e`, updated whenever the columns move.\n\n\n# 11 - The tick\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"covers/phase_time_passes.jpg\" alt=\"Time \u0026 passes phase\" style=\"max-height: 380px; max-width: 100%;\"\u003e\u003c/p\u003e\n\nA program's life has a shape:\n\n- **Start-up** - initialisation. Tables are allocated, inputs are opened, the RNG is seeded, the world reaches a known state.\n- **Steps** - ticks of the clock in a simulation, turns in a card game, event handlers in a server. The repeating unit of forward motion.\n- **Save and load** - the in-memory state is preserved to disk so a future run can resume from where this one left off. Optional, but if you want it, it lives here.\n- **Exit** - resources are returned to the kernel. Memory, file handles, sockets, lockfiles. Failure to do this cleanly is called a *memory leak* (or a stale lock, or a broken socket).\n\nThis section is about the step. The step is where the time budget binds, where the system DAG runs, where determinism either holds or breaks. The other phases are real and important - the book returns to save and load when persistence is named at [§36](#36---persistence-is-table-serialization), and exit is mostly the operating system's job - but the inner step is what makes or breaks every other property the book builds on.\n\nEach step is a *tick*. State at the start of a tick is read; state at the end is written; nothing is half-updated mid-tick. Even an interactive program - a card game waiting for the next move, a text editor waiting for a keystroke - is a tick loop, just with an external trigger driving it. A program that does a single pass over a file and exits is a degenerate tick loop with one tick: it has the same start-of-tick / end-of-tick contract, just with N=1.\n\nTicks come in two natural shapes.\n\nA **time-driven** tick fires at a fixed rate. The simulator from [`code/sim/SPEC.md`](https://root-11.codeberg.page/intro-book/code/sim/SPEC.html) runs at 30 Hz: one tick every 33 ms. The loop wakes up, advances every system by one step, sleeps until the next tick. Most simulations, games, control loops, audio engines, and animation systems are time-driven. The rate is a contract with the rest of the world: at this rate, output appears.\n\nA **turn-based** tick fires when an event arrives. A card game ticks when a player makes a move. A chess engine ticks when its opponent moves. A discrete-event simulator ticks at the timestamp of the next pending event, however far in the future that is. The clock advances *with* the events, not under them. Turn-based ticks have no fixed rate; their pace is set by the input stream.\n\nBoth are ticks. The difference is what triggers the next pass:\n\n```rust,no_run\n// time-driven\nuse std::time::{Duration, Instant};\nconst TICK: Duration = Duration::from_millis(33);\n\nloop {\n    let start = Instant::now();\n    run_all_systems(\u0026mut world);\n    let elapsed = start.elapsed();\n    if elapsed \u003c TICK {\n        std::thread::sleep(TICK - elapsed);\n    }\n}\n```\n\n```rust,no_run\n// turn-based\nloop {\n    let event = wait_for_next_event();\n    apply_event(\u0026mut world, event);\n}\n```\n\nThe §0 simulator runs time-driven. The card game from §5 ran turn-based - every card you dealt was one tick. Both are valid; both fit the same framework.\n\nWithin each tick, the systems run in an order specified by the system DAG ([§14](#14---systems-compose-into-a-dag)'s topic). Each tick has a *budget*: 33 ms at 30 Hz, the ms-per-move in a card game played at human speed. The budget binds the design: at 30 Hz with 1 000 000 creatures, each motion update has 33 nanoseconds, which only fits if the data layout cooperates ([§4](#4---cost-is-layout---and-you-have-a-budget) made this precise).\n\nA subtle pitfall worth naming. Mixing turn-based and time-driven thinking in the same loop produces *drift*: the turn-based subsystem's pace bleeds into the time-driven subsystem's budget. The fix is to keep the two cleanly separated - typically, one outer loop and the other as an event source feeding it.\n\nA tick is the unit of forward motion in any program that has forward motion. The next sections name what *fits* in one tick, in what order, and what does not.\n\n## Exercises\n\nYou will need a minimal Rust project for these. `cargo new tick_lab` is enough.\n\n1. **A 30 Hz time-driven loop.** Write a `main` that loops at 30 Hz. Each iteration, print the elapsed time since program start. Sleep between ticks to maintain the rate. Run it for 10 seconds. Did you actually get 300 iterations?\n2. **The naive sleep mistake.** Replace your sleep logic with `std::thread::sleep(Duration::from_millis(33))` (no measurement). Run for 30 seconds. Does the program drift over time? Why?\n3. **Dropped frames.** Inside the loop, sleep for 50 ms - longer than the budget. The loop is now running at 20 Hz; it has *missed frames*. Print a warning when this happens.\n4. **A turn-based loop.** Write a tiny REPL: print `\u003e `, read a line, print `you said: \u003cline\u003e`. Each line is one tick. Run it. Note that the loop has no fixed rate - its pace is your typing.\n5. **Mixing the two.** Modify exercise 4 so that, while waiting for input, the program also prints the current second once per second. (Hint: spawn a thread, use a non-blocking read, or interleave with timeouts.) Note how mixing the two patterns adds complexity quickly.\n6. *(stretch)* **A discrete-event tick loop.** Maintain a `Vec\u003c(f64, String)\u003e` of `(timestamp, message)` events. Pop the smallest-timestamp event, advance a \"simulation clock\" to that timestamp, print the message, repeat until the queue is empty. This is the structure of a discrete-event simulator and a preview of [§12](#12---event-time-is-separate-from-tick-time).\n\n## What's next\n\nExercise 6 hints at the next section. The clock can live on the events themselves, independent of how often the loop fires. [§12 - Event time is separate from tick time](#12---event-time-is-separate-from-tick-time) names that separation.\n\n\n# 12 - Event time is separate from tick time\n\nMost beginners assume the loop's frequency sets the model's time resolution. If the loop runs at 30 Hz, surely the model can only resolve events at 1/30 s = 33 ms? This is wrong, and the confusion costs many simulations their precision.\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"illustrations/oscilloscope_sine.jpg\" alt=\"An oscilloscope: sample rate is independent of signal frequency\" style=\"max-height: 300px; max-width: 100%;\"\u003e\u003c/p\u003e\n\nThe tick rate is *how often the loop runs*. It says nothing about what the loop does inside one tick. Inside one tick, the loop can process events at arbitrary timestamps - microsecond, picosecond, whatever the data carries. The clock lives on the events, not on the loop.\n\nConcretely: a 30 Hz loop receiving 1 000 events per tick, each with microsecond-precision timestamps, processes them in timestamp order - applying each event's effect with the precision the timestamp implies. Output to the rest of the world (rendering, logging, network) happens at 30 Hz, but the *physics inside* runs at microsecond resolution. The tick is a *sampling* rate; the events are the actual phenomena.\n\nThis is the model used by:\n\n- **Discrete-event simulators** (queueing networks, traffic, supply chains): events fired at exact times.\n- **Game replay systems** (rollback netcode, multiplayer): events arrive late but with their original timestamps.\n- **Trade execution engines**: orders carry nanosecond timestamps; the loop processes them in order.\n- **Logic simulators** in chip design: gate transitions at picosecond resolution; the simulator advances one transition at a time.\n\nIn each case, the tick rate of the host loop is irrelevant to the simulation's resolution. The data carries the time.\n\nThis separation is what makes the simulator's `pending_event` table possible. Each tick, the loop builds a list of events that should fire - collisions, eats, reproductions - each tagged with its predicted timestamp. The events fire in timestamp order regardless of which tick they were *predicted in*. A creature that \"would have eaten 2 µs into the tick\" has its eat applied at that exact moment, not at the start or end of the tick.\n\nThe pitfall is hard-coding the tick interval as the simulation's clock granularity. Code that says\n\n```rust,ignore\ncreature.energy -= 1.0 / 30.0; // \"one tick worth of fuel\"\n```\n\nis conflating the two clocks. The right shape is\n\n```rust,ignore\ncreature.energy -= elapsed_event_seconds * burn_rate;\n```\n\nusing the actual elapsed event-time, not the tick interval.\n\nEvent time and tick time are decoupled because they answer different questions. Event time answers *when did this thing happen*. Tick time answers *when does the loop wake up*. The same model can be sampled at any tick rate the application needs - visualisation at 30 Hz, recording at 60 Hz, fast-forward replay at 1 kHz - without changing what the model means.\n\n## Exercises\n\nThese extend the discrete-event loop from §11 exercise 6.\n\n1. **A tiny event queue.** Use `Vec\u003c(f64, String)\u003e` and `Vec::sort_by`. Push 10 events with random timestamps in `[0, 10]` seconds. Pop them in order; print each as `[t=\u003csec\u003e] \u003cmessage\u003e`. Verify the output is timestamp-sorted.\n2. **The wrong way: tick-rate clock.** Run a 30 Hz loop. In each tick, advance a counter by `1.0 / 30.0`. Use this counter as your \"simulation time\". Try to fire an event at `t = 0.005 s` (5 ms). What happens? When does the event fire?\n3. **The right way: timestamp on events.** Run the same 30 Hz loop, but each tick pop *all* events with timestamp ≤ current real time, applied in timestamp order. Fire an event at `t = 0.005 s`. Show that the event applies at exactly that time, not at the next tick boundary.\n4. **Sampling at different rates.** Run the same model under a 30 Hz loop, then a 60 Hz loop, then a 1 Hz loop. The events should fire at the same simulation times in all three runs (down to whatever precision the loop allows).\n5. **Float and time.** What's the smallest time step `f32` can represent for events at `t ≈ 1 hour`? At `t ≈ 1 day`? At `t ≈ 1 year`? When do you need `f64`? (See [§2](#2---numbers-and-how-they-fit).)\n6. *(stretch)* **A budget-aware loop.** Modify your 30 Hz loop: at the start of each tick, pop events until either (a) the queue is empty or (b) you have used 25 ms of the 33 ms budget. Defer remaining events to the next tick. This is the soft-real-time pattern used in interactive simulators.\n\n## What's next\n\n[§13 - A system is a function over tables](#13---a-system-is-a-function-over-tables) introduces the building block of every tick: the system. Read-set in, write-set out, no hidden state, no surprises.\n\n\n# 13 - A system is a function over tables\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"illustrations/differential_equations.jpg\" alt=\"A mouse at the chalkboard - systems are functions of state\" style=\"max-height: 300px; max-width: 100%;\"\u003e\u003c/p\u003e\n\nA *system* is a function that reads from one or more tables and writes to one or more tables. It declares its inputs (the *read-set*) and its outputs (the *write-set*). It has no hidden state, no global side effects, no interaction with the outside world during a tick. The signature is the contract.\n\n```rust,no_run\nfn motion(px: \u0026mut [f32], py: \u0026mut [f32], vx: \u0026[f32], vy: \u0026[f32], dt: f32) {\n    for i in 0..px.len() {\n        px[i] += vx[i] * dt;\n        py[i] += vy[i] * dt;\n    }\n}\n```\n\nRead-set: `vx`, `vy`, `dt`. Write-set: `px`, `py`. That is the entire contract. This system can run any time the velocity columns and `dt` are available and nothing else is writing the position columns.\n\nEvery system takes one of three shapes.\n\nAn **operation** is 1→1: every input row produces exactly one output row. `motion` is an operation: each creature's position is updated to its new position. Most update functions are operations.\n\nA **filter** is 1→{0, 1}: every input row produces zero or one output rows. `apply_starve` (from `code/sim/SPEC.md`) is a filter: each creature with energy ≤ 0 produces an entry in `to_remove`; creatures with energy \u003e 0 produce nothing.\n\nAn **emission** is 1→N: every input row produces zero or more output rows. `apply_reproduce` is an emission: a parent above the energy threshold produces two offspring (a 1→2 emission).\n\nThese three shapes are the same shapes a database query takes. `SELECT * FROM t WHERE p` is a filter, `SELECT a + b FROM t` is an operation, `SELECT explode(arr) FROM t` is an emission. A system is a database operation written in Rust against `Vec`s instead of SQL against tables.\n\nThe contract that the system has *no hidden state* is what makes systems compose. Two systems with disjoint write-sets can run in parallel without coordination ([§31](#31---disjoint-write-sets-parallelize-freely)). Two systems whose read-set and write-set form a chain must run in order ([§14](#14---systems-compose-into-a-dag)). The contract is the basis for all of this.\n\nEven *observability* is a system. A debug inspector is a system whose read-set is \"all tables\" and whose write-set is \"nothing\". It runs alongside the others, gathers data for inspection, and produces no side effects on the world. In production it is *absent*, not gated by a flag - the binary simply does not contain it.\n\nA few patterns to watch for. A function that reads a table, writes to it, and reads it again in the same call is *not* a system - it has implicit ordering inside the body. Either split it into two systems with explicit ordering, or buffer the writes until the function exits. A function that takes `\u0026mut World` and mutates whatever it likes is *not* a system - it has no declared write-set, and you cannot reason about it from its signature.\n\nA system declares its inputs, declares its outputs, and does no more. That is the shape that lets every other discipline in the book work.\n\n## Exercises\n\nUse the deck from §5 or the §0 simulator skeleton; either provides enough tables.\n\n1. **Identify the shape.** Classify each as operation, filter, or emission:\n   - Squaring every entry in a `Vec\u003cf32\u003e`.\n   - Filtering even integers from a `Vec\u003cu32\u003e`.\n   - Splitting each string in `Vec\u003cString\u003e` into words, returning all words.\n   - Computing the sum of a `Vec\u003cu32\u003e`.\n2. **Write motion as a system.** With position columns `px, py: Vec\u003cf32\u003e` and velocity columns `vx, vy: Vec\u003cf32\u003e`, write `fn motion(px: \u0026mut [f32], py: \u0026mut [f32], vx: \u0026[f32], vy: \u0026[f32], dt: f32)`. Apply it to 100 creatures with random initial positions and velocities. Print the position of one creature across 10 ticks.\n3. **Declare the contract.** Add doc comments to `motion` listing its read-set and write-set explicitly. The signature plus the doc comment is the system's contract.\n4. **Write a filter.** With `energy: \u0026[f32]`, write `fn starving(energy: \u0026[f32]) -\u003e Vec\u003cusize\u003e` returning the indices where `energy[i] \u003c= 0`. This is the read-only first half of `apply_starve`.\n5. **Write an emission.** With `parent_energy: \u0026[f32]`, threshold `threshold: f32`, write `fn reproduce(parent_energy: \u0026[f32], threshold: f32) -\u003e Vec\u003c(usize, f32)\u003e` returning, for each parent above threshold, two `(parent_index, offspring_energy)` entries. This is a 1→2 emission.\n6. **Observe non-systems.** Find a function in your previous work (or any tutorial) that mutates global state, writes to stdout in its body, or takes `\u0026mut World`. Note what makes it not a system.\n7. *(stretch)* **A test as a system.** Write `fn no_creature_moved_too_far(prev_px: \u0026[f32], prev_py: \u0026[f32], cur_px: \u0026[f32], cur_py: \u0026[f32]) -\u003e Vec\u003cusize\u003e`, returning indices where the move was implausibly large. The \"test\" is just an inspection system reading the world.\n\n## What's next\n\n[§14 - Systems compose into a DAG](#14---systems-compose-into-a-dag) takes the next step: when many systems run together, how do they fit?\n\n\n# 14 - Systems compose into a DAG\n\nA program with one system is uninteresting; a program with many systems must say *what runs in what order*. The order is given by data dependencies: a system that reads a table must run *after* every system that writes that table within the same tick. No ordering is fixed by intuition; everything is given by the read-sets and write-sets.\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"illustrations/dag_planning_checklist.jpg\" alt=\"PLAN / ANALYZE / DESIGN / BUILD / TEST / IMPROVE - the planning DAG\" style=\"max-height: 300px; max-width: 100%;\"\u003e\u003c/p\u003e\n\nDraw the dependency graph. Each system is a node. For every system that reads table `T` and every system that writes `T`, draw an edge `writer → reader`. The result is a directed acyclic graph (the DAG). A topological sort gives a valid execution order: any sort that respects the edges is correct. The program executes one such sort.\n\nThe simulator's tick from `code/sim/SPEC.md`:\n\n```mermaid\nflowchart TB\n    food_spawn --\u003e motion\n    motion --\u003e next_event\n    next_event --\u003e apply_eat\n    next_event --\u003e apply_reproduce\n    next_event --\u003e apply_starve\n    apply_eat --\u003e cleanup\n    apply_reproduce --\u003e cleanup\n    apply_starve --\u003e cleanup\n    cleanup --\u003e inspect\n```\n\n`food_spawn` runs first because its output is `food`, which `motion` and `next_event` read. `next_event` produces `pending_event`, which the three appliers consume in parallel (their write-sets are disjoint). `cleanup` runs after all of them because its read-set includes their writes. `inspect` runs last because it reads everything and writes nothing.\n\nThis is the same shape as a *query plan* in a database. The query optimiser takes a SQL statement, builds a graph of relational operations (each one a system!), and topo-sorts them into an execution plan. A simulator is a query plan running every tick.\n\nThe reason the graph must be acyclic is that a cycle is a contradiction. Suppose system A writes table T, system B reads T and writes U, system A reads U. Now A both produces T (which B reads) and consumes U (which B writes). A and B cannot both run before each other in the same tick. A cycle in the system graph is a design bug; it must be broken - usually by buffering one system's write so it is consumed *next* tick instead of *this* tick.\n\nDesigning system order is therefore the same problem as designing a database query plan. Each system is a stage; the DAG is the plan; the program executes the plan. Students who follow this thread end up writing their own minimal query engine without realising it.\n\nThe cost of getting the DAG wrong is concrete. A reader that runs *before* its writer reads stale data - yesterday's snapshot of a table that was supposed to have been updated. A reader that runs *after* its consumer reads garbage - a half-written table mid-update. The DAG is the contract that prevents both.\n\nA subtle benefit: once the DAG is explicit, parallelism becomes trivial. Any two systems on the same DAG level - neither one a transitive dependency of the other - can run on different threads. The schedule is implied by the graph. [§31](#31---disjoint-write-sets-parallelize-freely) picks this up.\n\n## Exercises\n\n1. **Draw the DAG.** Take the eight simulator systems (motion, food_spawn, next_event, apply_eat, apply_reproduce, apply_starve, cleanup, inspect) and draw the dependency graph yourself, deriving the edges from each system's read-set and write-set in `code/sim/SPEC.md`. Compare with the diagram above.\n2. **Spot the cycle.** Suppose `apply_starve` writes to `food` (returning fuel to the world when a creature dies). Now `apply_starve` writes `food`, which `food_spawn` reads. `food_spawn` writes `food`, which `next_event` reads. `next_event` writes `pending_event`, which `apply_starve` reads. Where's the cycle? How would you break it?\n3. **Topological sort by hand.** Given:\n\n   - A writes X\n   - B reads X, writes Y\n   - C reads X, writes Z\n   - D reads Y and Z, writes W\n\n   Which systems can run in parallel? What's a valid execution order? Are there multiple valid orders?\n4. **Compose two systems.** Write `motion` (operation, writes `pos`) and `next_event` (operation, writes `pending_event`). Wire them into a tick that runs `motion` then `next_event`. Inspect `pending_event` after the tick.\n5. **Add `cleanup`.** Add a `cleanup` system that processes `to_remove` and `to_insert` (both initially empty `Vec`s). Wire it after `next_event`. Confirm the DAG remains acyclic.\n6. *(stretch)* **A query planner.** Take five hand-written SQL queries (each one a system shape) and draw the relational-algebra plan for each. Compare with how `motion → next_event → apply_*` decomposes the simulator. The shape is the same.\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"illustrations/tip_visualize_full.jpg\" alt=\"Visualize the problem. A good diagram can reveal the solution.\" style=\"max-height: 300px; max-width: 100%;\"\u003e\u003c/p\u003e\n\n## What's next\n\n[§15 - State changes between ticks](#15---state-changes-between-ticks) is the rule that makes the DAG actually work: mutations buffer; the world transitions atomically.\n\n\n# 15 - State changes between ticks\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"illustrations/microcontroller_loop.jpg\" alt=\"Init / while { read; process; update } - the visible tick loop\" style=\"max-height: 300px; max-width: 100%;\"\u003e\u003c/p\u003e\n\nInside a tick, the *population* is frozen: no creature is born or dies mid-tick. Structural changes - insertions and removals - are *queued*, not applied, and committed in one atomic sweep at the tick boundary. Value updates are different: they flow along the DAG in tick order, each system reading its inputs as the upstream writers left them, never half-written.\n\nThis is the rule that makes the DAG from [§14](#14---systems-compose-into-a-dag) actually work. The danger is a system reading another's half-finished work: if `next_event` began reading `pos` while `motion` were still writing it, half the creatures would have moved and half would not, and what `next_event` reads would no longer be well-defined. Two rules remove the danger. *Order*: a system runs only after the systems it reads from have *completed*, so it sees their finished output, never a partial write. *Frozen membership*: no system adds or removes a row mid-tick, so the *set* of creatures every system iterates is identical. Together they make the tick a clean function `world_t+1 = step(world_t, inputs_t)` - values move forward along the DAG, the population holds still, and `cleanup` commits the queued births and deaths only at the boundary.\n\nConcretely: `apply_starve` does not call `creatures.swap_remove(slot)`. It calls `to_remove.push(creature_id)`. The `creatures` table is unchanged for the rest of the tick. After every system has run, `cleanup` consumes `to_remove` and `to_insert` together, applying every queued change in one sweep. *Now* the next tick begins with a consistent new world state.\n\nThis pattern is called *double buffering*: there is the committed world the systems read this tick, and the buffer of queued changes (`to_remove`, `to_insert`) that `cleanup` commits to produce the world the next tick reads. The pattern shows up everywhere - graphics frame buffers, database transactions, event-sourced systems. The rule is always the same: structural writes accumulate, then commit.\n\nTwo costs to absorb. First, every queued birth or death is one extra row pushed to a `to_remove` or `to_insert` table. Second, the cleanup pass is now its own system in the DAG. The benefit dwarfs the costs: every other system in the book composes cleanly, and parallelism becomes easy. With in-tick mutation, every parallel scheduling decision becomes a race condition. With buffered mutation, races are structurally impossible - disjoint write-sets are disjoint by construction.\n\nA subtle case is *insertions*. A creature born during a tick (via `apply_reproduce`) does not appear in any system's read-set during that tick - it is in `to_insert`, not in `creatures`. The newborn lives its first life on the *next* tick. This is the right behaviour for almost every simulation: it gives every creature an equal first tick of life. The alternative - applying inserts mid-tick - is a closed-loop bug factory.\n\nWithin one system, the writes *can* be in-tick: a system that updates `pos` for every creature in a loop applies each write immediately, because the rest of the system is the only reader and the only writer. The buffering rule is between *systems*, not between iterations within one system. Inside a system, the writes are sequential; between systems, the writes are batched.\n\nThe shape that emerges is: read everything into local arrays at system entry; do work; write outputs to buffers at system exit; commit at tick boundary. It is the same shape as the audio engine's frame buffer, the database's transaction commit, and the version-controlled file system's commit-and-merge. They all solve the same problem: how do you read consistent state while the world is changing?\n\n## Exercises\n\nThese build on the simulator skeleton. Your `to_remove: Vec\u003cu32\u003e` and `to_insert: Vec\u003cCreatureRow\u003e` should already exist.\n\n1. **The bug.** Write a function that iterates `creatures` and calls `creatures.swap_remove(i)` whenever `energy[i] \u003c= 0.0`. Run it on a 100-creature world where 30 are starving. What goes wrong? (Hint: skipped iterations, half the starvers survive.)\n2. **The fix.** Rewrite the function to push the index into `to_remove` instead. After the loop completes, apply all removals in one pass. Verify all 30 starvers die.\n3. **The cleanup pass.** Write `fn cleanup(world: \u0026mut World, to_remove: \u0026mut Vec\u003cu32\u003e, to_insert: \u0026mut Vec\u003cCreatureRow\u003e)`. Apply removals first (using `swap_remove`), then insertions. Why this order, and not the other?\n4. **Show two ticks.** Run the loop for two ticks. After tick 1, log the population. After tick 2, log it again. Confirm that creatures killed in tick 1's `apply_starve` *do not* appear in tick 2's input.\n5. **Insertions are tick-delayed.** A creature reproduces in tick 5: parent in `creatures`, two offspring in `to_insert`. After cleanup, the offspring are in `creatures`. In tick 6 the offspring receive their first system pass. Confirm by adding an `age_in_ticks` column and watching offspring start at 0 in tick 6, not in tick 5.\n6. *(stretch)* **A bad design that almost works.** Try to apply mutations in-tick *carefully* - collect dead creatures first, then process them in reverse-index order. Show one specific case where this still corrupts state. (Hint: a reproduction produces an offspring whose new index conflicts with an in-progress death.)\n\n## What's next\n\n[§16 - Determinism by order](#16---determinism-by-order) is the property the buffering rule *guarantees*: same inputs, same system order, same outputs. Reproducibility is structural.\n\n\n# 16 - Determinism by order\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"illustrations/monte_carlo.jpg\" alt=\"Monte Carlo estimate of π - same seed, same answer, every run\" style=\"max-height: 300px; max-width: 100%;\"\u003e\u003c/p\u003e\n\nA program is *deterministic* if the same inputs and the same execution produce the same outputs, every time. Sounds obvious. It is not - most modern programs are *not* deterministic by default. Threads run in OS-scheduled order. Hash maps may iterate in randomised order. The system clock differs by run. `rand::thread_rng()` differs by process.\n\nIn an ECS architecture, determinism is structural. Same world state at tick start + same system order + same inputs (events, RNG seed) = same world state at tick end. Bit-identical. Every time.\n\nThis is not a quality goal; it is a precondition for almost everything the book builds on:\n\n- **Replay.** The world is the log decoded ([§37](#37---the-log-is-the-world)). Replay reconstructs world state by re-running the inputs through the same system sequence. Without determinism, replay is impossible.\n- **Testing.** A property test fixes an RNG seed and asserts the simulator behaves identically across runs. Without determinism, every test is flaky.\n- **Distributed simulation.** Multiple machines run identical copies of the world. Without determinism, they drift apart by tick 1.\n- **Debugging.** A bug at tick 4783 should appear at tick 4783 every run. Without determinism, debugging real-time bugs becomes guesswork.\n\nThe recipe for determinism is simple: forbid every source of non-determinism in the inner systems.\n\n- **No `HashMap` in iteration order.** Use `Vec` or `BTreeMap`, which iterate in deterministic order.\n- **No system clock.** Get time from input events, not from `Instant::now()`. Time is a value passed *into* the system, not read from the OS.\n- **One RNG, seeded.** A single `Rng` with a fixed seed, used in a defined order. Each system that needs randomness reads from it in DAG order.\n- **No threads inside a system.** A system runs single-threaded internally. Parallelism happens *between* systems with disjoint write-sets ([§31](#31---disjoint-write-sets-parallelize-freely)), not inside one system.\n- **Buffered mutations.** [§15](#15---state-changes-between-ticks)'s rule: mutations apply at tick boundaries, not mid-tick.\n\nThese rules are restrictive. They are also the price of every benefit listed above. Most modern programs decline to pay this price and accept the costs - flaky tests, unreproducible bugs, divergent distributed simulation. The book pays the price.\n\nThe cost of determinism is not absolute. *Within* a system, the implementation is free to use whatever it likes - SIMD intrinsics, branch hints, compile-time tricks - as long as the inputs and outputs are bit-identical to what the abstract specification demands. The discipline is at the system boundary: between systems, everything must be reproducible.\n\n\u003e [!NOTE]\n\u003e **Parallel reductions are the exception.** \"Bit-identical\" is easy to lose the moment a reduction runs in parallel. Floating-point addition is not associative: `(a + b) + c` is not always `a + (b + c)` in the last bits. A parallel sum that splits the data across threads and merges the partials adds in a different grouping than a serial sum, so the *same seed with a different thread count* can produce different bits. That is the canary: if your world hashes diverge only when you change core count, suspect a parallel floating-point reduction. Two escape hatches keep determinism. Fix the reduction order: always fold the partials in a defined sequence, independent of how many threads produced them. Or accumulate in integers: scale to fixed-point, sum exactly, scale back. Integer accumulation is exact by construction; fixed-order floating-point is reproducible but still rounds. This is the determinism gotcha behind the replay-across-heterogeneous-hardware claim above - drop it and \"same seed, same answer\" quietly stops being true across machines with different core counts.\n\nA test for determinism is concrete. Run the simulator twice with the same seed, the same input event log, the same system order. After 1 000 ticks, hash the entire world state. If the hashes match, you are deterministic. If they do not, find the system whose output first differs, and trace the source of variability. Often: a `HashMap`, a system clock, a thread.\n\nA simulator that is deterministic is also a simulator that *can be tested*. Once that property holds, every other quality goal - performance, parallelism, distribution - becomes safe to optimise toward. Without determinism, every optimisation is a coin flip.\n\nThe full payoff of determinism arrives at the *save and load* phase named in [§11](#11---the-tick). The simulator can be paused, its tables serialised to disk, reloaded later, and resumed - and the result must be indistinguishable from a run that never paused. The mechanics arrive in [§36 - Persistence is table serialization](#36---persistence-is-table-serialization): a snapshot is the world's tables written as a stream of `(entity, key, value)` triples - the same shape they have in memory. Combined with the input event log, replay is","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Froot-11%2Fintro-book","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Froot-11%2Fintro-book","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Froot-11%2Fintro-book/lists"}