The Problem
I can write software that works. What I kept finding is that I couldn't always explain why it works at the level below my abstractions. I knew that passing a large struct by pointer was faster than by value. I knew that cache misses were expensive. I knew that null pointer dereferences caused crashes. But this was knowledge I held as received wisdom, not something I could trace through a mental model with any confidence.
Security work made the gap harder to ignore. Reading disassembly, reasoning about buffer overflows, understanding why certain memory layouts are exploitable: these require you to think in addresses, not variable names. Debuggers show you register values and you have to know what they mean. You encounter a segfault and need to know immediately whether you're chasing a null pointer, a use-after-free, or a stack overflow.
I looked for something interactive. Textbooks explained the concepts correctly but passively: reading about a pointer chain is not the same as tracing one yourself. YouTube videos moved at someone else's pace. University course notes were thorough but assumed a semester's context I didn't have time to reconstruct.
What I wanted was a tool that forced active engagement: read the concept, immediately simulate it, then test yourself before moving on. Nothing I found matched that loop tightly enough.
So I built Lolevel.
Design Constraints
The constraints followed from how I wanted to use it:
- No installation, no setup: Browser-based so it loads anywhere, immediately
- Zero dependencies: Three vanilla files (
index.html,style.css,app.js), no framework, no bundler, no build step - Concept → simulation → quiz: Each module had to include a narrative explanation, an interactive exercise, and a knowledge check before the next module unlocked
- Analogies over jargon: The memory address section needed to land before the pointer section could build on it; every explanation had to earn the next one
- Persistent progress:
localStorageto track completed modules and quiz scores across sessions without a backend
The goal was a self-contained learning environment I could open mid-task when I needed to rebuild intuition around a specific concept, not a course that demanded a block of uninterrupted time.
Implementation
The project is three files. The application is three modules, each structured identically: a narrative with a code sample, an interactive simulation, and a five-question quiz. Completing a module's quiz with at least 4/5 unlocks the next. Module 1: Memory & Addresses introduces RAM as a warehouse of numbered boxes, explains why addresses exist, and lets you write bytes into a simulated 16-cell memory grid. You pick an address, type a value in 0–255, and watch it appear alongside its binary representation and ASCII character. A scrolling log beneath the grid records every STORE and LOAD operation. Module 2: Pointers & Indirection builds directly on Module 1. The interactive is a pointer chain explorer: a memory table where each row shows an address, variable name, stored value, and type. You step through the chain one level at a time. Three scenarios are included: a simple Module 3: Registers & CPU covers why registers exist, the x86-64 register file, the Program Counter, and the cache hierarchy. The interactive is a register allocator: you select an operation, pick operands, and execute. The task is to compute The optimal solution is 203 cycles: three LOADs, one ADD, one MUL, one STORE. Reaching it requires noticing that you can reuse a register rather than loading Each module's content is a plain object in A reference glossary sits alongside the modules. Fifteen terms total, five unlocked per completed module. You can't read the definition of "segmentation fault" until you've worked through the pointer module that contextualises it. Passive reading about pointers doesn't build the mental model you need when you're staring at a debugger. Tracing a pointer chain yourself, clicking through The NULL pointer scenario drives this home. It's one thing to know that dereferencing NULL causes a segfault. It's another to follow address The cycle counter in the register allocator creates the same effect for performance intuition. After executing a LOAD, an ADD, and a STORE and seeing the tally read 201, the advice "minimise memory accesses" stops being a guideline and becomes something you have observed. The project is deployed on Cloudflare Workers using the static assets feature: The locked/unlocked progression enforces the conceptual dependency order the topics actually have. You can't reach the register module without understanding memory addresses, because the register module uses address notation that Module 1 established. I'm planning two more modules: the call stack (stack frames, Interactivity is not decoration. I built the simulations because I needed them, not because they made the project look more complete. The moment I traced the double-pointer chain myself rather than reading a diagram, I understood it differently. The tool is more effective because the interactive components are load-bearing, not cosmetic. No build toolchain is a real constraint, not a compromise. Three plain files with no bundler forced every feature to justify its weight. There's no analytics, no login, no progress sync: just the learning loop. That constraint meant I shipped something usable quickly instead of designing infrastructure for months. The files are also trivially auditable and deployable anywhere that serves static files. Build the prerequisite into the product. The locked module progression isn't friction; it's the pedagogy. Concepts in computer architecture have hard dependencies. Respecting those dependencies in the UI means a user who reaches Module 3 actually has the foundation for it, rather than arriving confused because they skipped Module 1. Encoding the learning path into the product was more useful than writing a note saying "read these in order." This is a small project. It won't appear on any list of impressive engineering work. But it's exactly the kind of tool I find most valuable in practice: a focused artifact that fills a specific gap in understanding, usable in five minutes, with no ceremony. I document projects like this because the pattern they represent (identify a conceptual gap, build the minimal thing that closes it, use it consistently) is more replicable than any individual project. The tool itself will eventually be superseded. The habit of building what you actually need, and making it simple enough to actually use, doesn't get superseded.index.html is a 13-line shell: charset declaration, a to style.css, a . Everything visible in the browser is rendered by JavaScript into that div. style.css is about 1000 lines of vanilla CSS with a dark theme and CSS custom properties for the colour system. app.js is about 1450 lines of vanilla JavaScript that owns both the data and the rendering logic.int, a double pointer (int*), and a NULL pointer that terminates with a simulated SIGSEGV to show exactly what happens when you dereference 0x00.(x + y) * z using four registers and four RAM variables, with a running cycle counter accumulating the cost of each instruction:const OPS = [
{ op: "LOAD", cyc: 100, desc: "Load a RAM variable into a register" },
{ op: "ADD", cyc: 1, desc: "Add two registers, store in dest" },
{ op: "MUL", cyc: 3, desc: "Multiply two registers, store in dest" },
{ op: "STORE", cyc: 100, desc: "Store a register value to a RAM variable" },
{ op: "MOV", cyc: 1, desc: "Copy a register value to another register" },
];
z twice.app.js:{
id: 1,
title: "Memory & Addresses",
story: [
"Imagine a massive warehouse stretching for miles...",
"The number painted on each box is its <strong>memory address</strong>...",
"Modern computers use 64-bit addressing, which allows for 2<sup>64</sup> unique addresses...",
],
storyCode: '// Variables live at memory addresses:\nint score = 100; // 0x7ffd2a04\nint lives = 3; // 0x7ffd2a08',
quiz: [
{ q: "What does a memory address represent?", opts: [...], ok: 1, ex: "..." },
...
],
refTerms: ["Byte", "Bit", "Memory Address", "RAM", "Volatile Memory"],
}
Why This Approach Works
0x28 -> 0x18 -> 0x08 -> 99, does something different cognitively. You're not memorising a rule about indirection; you're practising the procedure until it becomes automatic.0x00 into a table cell labelled (null) and read "the OS delivers SIGSEGV and kills the process" as the conclusion of steps you just walked through. The simulation gives the rule a shape.Current State
wrangler.json points assets.directory at the project root and Cloudflare serves the three files directly with no worker script in between. No routing rules, no redirects, no server logic of any kind.{
"name": "lolevel",
"compatibility_date": "2026-04-12",
"assets": { "directory": "./" }
}
rsp, how function calls work at the assembly level) and virtual memory (page tables, the OS abstraction over physical RAM). Both are natural extensions of what's already there and already seeded in the reference glossary.Lessons
Why I'm Documenting This