From ea34b7cb52e933ab2f95c66194ed0c84b32abb95 Mon Sep 17 00:00:00 2001 From: Oleksandr Kozachuk Date: Mon, 13 Apr 2026 10:52:47 +0200 Subject: [PATCH] Add learning tools: Anki deck, IR quiz, reading order, trace exercises MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tools/anki_gen.py: generates 389-card Anki deck (.apkg) from hand-crafted YAML + auto-parsed source (IrOp variants, memory constants, error types, peephole patterns, primitive registrations, boot.fth defs, Runtime trait). tools/anki_data.yaml: 71 hand-crafted cards covering architecture, design decisions, ForthVM internals, codegen, optimizer, boot.fth, control flow, Runtime trait, and testing infrastructure. tools/ir_quiz.py: interactive terminal quiz (41 exercises) — predict optimized IR for Forth code (constant fold, peephole, strength reduce, DCE, tail call, inlining). tools/reading_order.md: guided 23-step codebase reading sequence. tools/trace_exercises.md: 20 trace-the-compilation exercises with answers. tools/architecture.txt: single-page ASCII system reference. --- tools/anki_data.yaml | 718 +++++++++++++++++++++++++++++++++++++++ tools/anki_gen.py | 687 +++++++++++++++++++++++++++++++++++++ tools/architecture.txt | 306 +++++++++++++++++ tools/ir_quiz.py | 347 +++++++++++++++++++ tools/reading_order.md | 189 +++++++++++ tools/trace_exercises.md | 464 +++++++++++++++++++++++++ tools/wafer_anki.apkg | Bin 0 -> 250074 bytes 7 files changed, 2711 insertions(+) create mode 100644 tools/anki_data.yaml create mode 100644 tools/anki_gen.py create mode 100644 tools/architecture.txt create mode 100644 tools/ir_quiz.py create mode 100644 tools/reading_order.md create mode 100644 tools/trace_exercises.md create mode 100644 tools/wafer_anki.apkg diff --git a/tools/anki_data.yaml b/tools/anki_data.yaml new file mode 100644 index 0000000..d7f9e77 --- /dev/null +++ b/tools/anki_data.yaml @@ -0,0 +1,718 @@ +# WAFER Anki Card Data +# Hand-crafted cards for architecture, design decisions, and "why" questions. +# Auto-generated cards (IrOp variants, memory addresses, etc.) are created by anki_gen.py. + +# ============================================================================ +# CATEGORY A: Architecture +# ============================================================================ +architecture: + + - front: "What are the 5 stages of the WAFER compilation pipeline?" + back: "Forth Source → Outer Interpreter (tokenize + dispatch) → IR (Vec) → Optimizer (6 passes) → WASM Codegen (wasm-encoder) → wasmtime execution" + code: | + // lib.rs doc comment: + // Forth Source -> Outer Interpreter -> IR -> Optimize -> WASM Codegen + source: "crates/core/src/lib.rs:9" + tags: [architecture, basic] + + - front: "What crate does WAFER use to generate WASM bytecode?" + back: "`wasm-encoder` — builds WASM modules programmatically (types, imports, functions, code sections). NOT a text-format assembler." + source: "crates/core/src/codegen.rs:12" + tags: [architecture, basic] + + - front: "What crate does WAFER use to execute WASM modules?" + back: "`wasmtime` — Bytecode Alliance WASM runtime. Provides Engine, Store, Module, Instance, Memory, Global, Table, Func." + source: "crates/core/src/outer.rs:14" + tags: [architecture, basic] + + - front: "How many source files are in wafer-core? Name them." + back: "12 files: outer.rs (9820), codegen.rs (4205), optimizer.rs (1013), dictionary.rs (906), export.rs (409), runner.rs (402), ir.rs (259), consolidate.rs (169), memory.rs (148), error.rs (84), config.rs (61), lib.rs (28), js_loader.rs (163)" + tags: [architecture, basic] + + - front: "What is the relationship between a Forth word and a WASM module in WAFER?" + back: "Each compiled Forth word becomes its own WASM module with one function. Modules share memory, globals (dsp/rsp/fsp), and a function table via wasmtime imports. Words call each other via call_indirect through the shared table." + tags: [architecture, intermediate] + + - front: "What are the 6 imports every WAFER WASM module expects?" + back: | + 1. emit (func: i32 → void) — character output callback + 2. memory (16 pages = 1 MiB) — shared linear memory + 3. dsp (global mut i32) — data stack pointer + 4. rsp (global mut i32) — return stack pointer + 5. fsp (global mut i32) — float stack pointer + 6. table (funcref) — shared function table for call_indirect + source: "crates/core/src/codegen.rs:25-41" + tags: [architecture, intermediate] + + - front: "What are the 3 types of words in WAFER?" + back: | + 1. IR primitives — compiled to WASM via Vec, inlineable (DUP, +, @) + 2. Host functions — Rust closures in wasmtime, NOT inlineable (., .S, M*, ACCEPT) + 3. Forth-defined words — compiled by outer interpreter (: SQUARE DUP * ;) + source: "crates/core/src/outer.rs:2422-2478" + tags: [architecture, basic] + + - front: "What is the role of outer.rs in WAFER?" + back: "Contains ForthVM — the complete Forth virtual machine, generic over execution backend. Outer interpreter (tokenize → lookup → interpret/compile), all primitive registration, control-flow compilation, DOES> support. 8703 lines." + source: "crates/core/src/outer.rs:1" + tags: [architecture, basic] + + - front: "What is the Runtime trait and why does it exist?" + back: | + Defined in runtime.rs. Abstracts over WASM execution backends: + - Memory r/w (mem_read_i32, mem_write_slice, etc.) + - Globals (get/set_dsp, rsp, fsp) + - Table (table_size, ensure_table_size) + - Module lifecycle (instantiate_and_install, call_func) + - Host functions (register_host_func) + Two implementations: NativeRuntime (wasmtime), WebRuntime (js-sys). + ForthVM is completely decoupled from any specific WASM engine. + source: "crates/core/src/runtime.rs:72" + tags: [architecture, intermediate] + + - front: "What is HostAccess and how is it used?" + back: | + Trait for memory/global access from within host function callbacks. + Methods: mem_read_i32, mem_write_i32, mem_read_u8, mem_write_u8, mem_read_slice, mem_write_slice, mem_len, get/set_dsp, get/set_rsp, get/set_fsp, call_func. + NativeRuntime implements it via CallerHostAccess (wrapping wasmtime::Caller). + WebRuntime implements it via WebHostAccess (wrapping js_sys typed arrays). + HostFn = Box Result<()>> — same closure works on both runtimes. + source: "crates/core/src/runtime.rs:17" + tags: [architecture, intermediate] + + - front: "What are the 3 crates in the WAFER workspace?" + back: | + 1. wafer-core — compiler, optimizer, codegen, dictionary, Runtime trait, outer interpreter + Feature flags: default=["native"], "native" enables wasmtime + 2. wafer (cli) — CLI REPL (rustyline), wafer build/run commands + 3. wafer-web — browser REPL (wasm-bindgen + WebRuntime + HTML/CSS/JS frontend) + source: "Cargo.toml workspace" + tags: [architecture, basic] + + - front: "What is NativeRuntime?" + back: | + Wasmtime-based implementation of Runtime trait (runtime_native.rs, 328 lines). + Owns: Engine, Store, Memory, Table, Globals (dsp/rsp/fsp), emit_func. + instantiate_and_install: Module::new + Instance::new with 6 imports. + register_host_func: creates wasmtime Func that bridges HostFn → CallerHostAccess. + Behind "native" feature flag. + source: "crates/core/src/runtime_native.rs:107" + tags: [architecture, intermediate] + + - front: "What is WebRuntime?" + back: | + Browser-based implementation of Runtime trait (crates/web/runtime_web.rs, 542 lines). + Uses js_sys::WebAssembly for module instantiation. + Memory: JsValue wrapping WebAssembly.Memory, accessed via Int32Array/Uint8Array views. + Globals: JsValue wrapping WebAssembly.Global objects. + Host functions: JS closures created via Closure::wrap, stored in _closures Vec to prevent GC. + Runs entirely in the browser — no wasmtime dependency. + source: "crates/web/src/runtime_web.rs:12" + tags: [architecture, intermediate] + + - front: "What is WaferRepl?" + back: | + The wasm-bindgen entry point in crates/web/src/lib.rs. + Wraps ForthVM. + Methods: new() (create VM), evaluate(input) (returns output), data_stack(), is_compiling(), reset(). + Built with: wasm-pack build --target web --out-dir www/pkg + source: "crates/web/src/lib.rs:13" + tags: [architecture, intermediate] + + - front: "What feature flags does wafer-core have?" + back: | + default = ["native"] + native — enables dep:wasmtime and all native-only modules: + runtime_native.rs, runner.rs, export.rs, consolidate.rs, js_loader.rs + Without "native" — pure Rust only: dictionary, IR, optimizer, codegen, outer interpreter, runtime trait. + wafer-web uses wafer-core without "native" feature. + source: "crates/core/Cargo.toml:11-12" + tags: [architecture, intermediate] + + - front: "What is the role of codegen.rs?" + back: "Translates optimized IR (Vec) to WASM bytecode using wasm-encoder. Handles DSP caching, scratch locals, stack-to-local promotion. Builds complete WASM modules including imports, types, function sections." + source: "crates/core/src/codegen.rs:1" + tags: [architecture, basic] + + - front: "What is batch_mode in ForthVM and why does it exist?" + back: "During boot (register_primitives), batch_mode=true defers WASM compilation. All ~40 IR primitives are collected, then compiled into a single WASM module via compile_batch(). This amortizes runtime compilation overhead — one rt.instantiate_and_install() instead of 40." + code: | + self.batch_mode = true; + // ... register all primitives ... + // deferred_ir.push((word_id, ir_body)); + self.compile_batch()?; + source: "crates/core/src/outer.rs" + tags: [architecture, advanced] + +# ============================================================================ +# CATEGORY B: Design Decisions ("Why" cards) +# ============================================================================ +design_decisions: + + - front: "Why does each Forth word compile to its own WASM module?" + back: | + 1. Incremental compilation: defining a new word doesn't recompile anything + 2. Isolation: each word is independently validated by wasmtime + 3. wasmtime linking model: modules share imports (memory, globals, table) + 4. REPL-friendly: immediate feedback, no whole-program recompile + Trade-off: call_indirect overhead between words (mitigated by CONSOLIDATE) + tags: [design, advanced] + + - front: "Why use an IR instead of compiling Forth directly to WASM?" + back: | + 1. Optimization: IR enables peephole, constant folding, inlining, DCE, tail calls + 2. Separation of concerns: outer interpreter doesn't need to know WASM encoding + 3. Portability: IR could target other backends + 4. Testability: IR is easy to inspect and test + 5. Consolidation: IR bodies are stored for later recompilation into single module + tags: [design, advanced] + + - front: "Why does WAFER use wasm-encoder + wasmtime instead of Cranelift directly?" + back: | + 1. Standard WASM: output is valid .wasm, can run in browsers AND natively + 2. Runtime abstraction: same WASM bytes work on wasmtime (CLI) and browser (js-sys) + 3. wasmtime handles Cranelift internally — best JIT performance for free + 4. Portability: WASM is platform-independent + 5. Validation: wasmtime validates modules, catching codegen bugs + 6. wasm-encoder is simple: just build bytes, no complex IR + tags: [design, advanced] + + - front: "Why was the Runtime trait introduced?" + back: | + To support the browser REPL without duplicating the entire ForthVM. + Before: ForthVM directly owned wasmtime types (Engine, Store, Memory, etc.) + After: ForthVM is generic — same compiler code works with: + - NativeRuntime (wasmtime) for CLI/tests/AOT + - WebRuntime (js-sys) for browser + Host functions use HostFn = Box — one closure definition serves both runtimes. + The refactor extracted ~1100 lines of wasmtime-specific code from outer.rs into runtime_native.rs. + tags: [design, advanced] + + - front: "Why are stacks in linear memory instead of WASM locals?" + back: | + Default: stacks live in linear memory (data stack at 0x0600, grows down). + Reason: Forth semantics require stack introspection (DEPTH, PICK, SP@), which WASM locals can't provide. + Optimization: stack-to-local promotion lifts values into WASM locals when the compiler can prove stack depth is statically known (no calls, no SP@). Best of both worlds. + tags: [design, advanced] + + - front: "Why is the DSP cached in a WASM local?" + back: | + The data stack pointer (dsp) is a wasmtime global. Globals are slower than locals. So: + 1. At function entry: local.get $dsp_global → local.set $cached_dsp + 2. During function: all stack ops use local $cached_dsp + 3. Before calls: write back local → global (callee needs correct dsp) + 4. After calls: reload local from global (callee may have changed dsp) + 5. At function exit: write back + Net effect: most operations avoid global access overhead. + code: | + const CACHED_DSP_LOCAL: u32 = 0; + fn dsp_writeback(f) { local.get 0; global.set $dsp } + fn dsp_reload(f) { global.get $dsp; local.set 0 } + source: "crates/core/src/codegen.rs:56-181" + tags: [design, advanced] + + - front: "Why does boot.fth replace Rust host functions with Forth definitions?" + back: | + 1. Self-hosting goal: maximize Forth, minimize Rust + 2. Performance: compiled Forth with inlining + optimization beats host function dispatch (call_indirect → Rust closure has overhead) + 3. Inlinability: Forth definitions have IR bodies that the optimizer can inline; host functions cannot be inlined + 4. Consolidation: Forth words participate in single-module recompilation (direct calls); host functions always use call_indirect + source: "crates/core/boot.fth:1-3" + tags: [design, advanced] + + - front: "Why does WAFER use -1 (all bits set) for TRUE instead of 1?" + back: | + Forth 2012 standard: TRUE = -1 (0xFFFFFFFF). All bits set. + Reason: allows bitwise AND as a conditional select: flag AND value. + If TRUE were 1, AND would only preserve the lowest bit. + With -1: TRUE AND x = x (identity). FALSE AND x = 0. + code: | + // In codegen, bool_to_forth_flag: + // 0 - result: if result=1 => -1, if result=0 => 0 + f.instruction(&I32Const(0)); + f.instruction(&LocalGet(tmp)); + f.instruction(&I32Sub); + source: "crates/core/src/codegen.rs:214-222" + tags: [design, intermediate] + + - front: "Why does the optimizer run peephole 5 times across the pipeline?" + back: | + Each optimization pass can create new peephole opportunities: + - After inline: inlined body may have adjacent ops that simplify + - After constant fold: folded constants may create identity patterns (PushI32(0), Add) + - After strength reduce: new patterns from reduced ops + - After DCE: dead code removal may leave adjacent simplifiable ops + The peephole pass itself runs to fixpoint (inner loop), but the outer pipeline runs it 5 times at different stages. + code: | + // Phase 1: peephole → fold → strength → peephole + // Phase 2: inline → peephole → fold → strength → peephole + // Phase 3: dce → peephole + // Phase 4: tail_call + source: "crates/core/src/optimizer.rs:37-85" + tags: [design, advanced] + + - front: "Why is tail call detection the LAST optimizer pass?" + back: | + 1. TailCall emits WASM `return` after the call — if inlining converts TailCall back to Call (detailcall), early tail-call detection is wasted + 2. DCE might eliminate the tail position entirely + 3. Need return-stack balance check on FINAL IR, not intermediate + 4. Inlining must happen first so we know which calls remain + source: "crates/core/src/optimizer.rs:79-84" + tags: [design, advanced] + + - front: "Why can't words with Exit be inlined?" + back: | + WASM `return` exits the CURRENT function. If an inlined word contains Exit (→ return), it would exit the CALLER's function, not just the inlined code. There's no 'return from inline' in WASM. The contains_exit() guard prevents this. + code: | + fn contains_exit(ops: &[IrOp]) -> bool { + // Also blocks ForthLocalGet/Set — would collide with caller's locals + matches!(op, IrOp::Exit | IrOp::ForthLocalGet(_) | IrOp::ForthLocalSet(_)) + } + source: "crates/core/src/optimizer.rs:633-664" + tags: [design, advanced] + + - front: "Why does CONSOLIDATE exist?" + back: | + Normal JIT: each word = separate module, calls via call_indirect (table lookup). + CONSOLIDATE: merges all JIT-compiled words into ONE module. + - call_indirect → direct `call` (for words in the module) + - wasmtime can optimize across call boundaries + - ~2-3x speedup for call-heavy code + External calls (host functions) remain call_indirect. + source: "crates/core/src/consolidate.rs:1-9" + tags: [design, advanced] + + - front: "Why does WAFER use a linked list for the dictionary instead of a hash map?" + back: | + 1. Forth standard specifies linked-list traversal semantics (TRAVERSE-WORDLIST) + 2. Dictionary lives in linear memory (simulates WASM memory layout) + 3. Standard requires specific entry format (link + flags + name + code field) + BUT: WAFER also has a HashMap index for O(1) fast-path lookup, falling back to linked-list walk for words not yet indexed. Best of both worlds. + source: "crates/core/src/dictionary.rs:10-48" + tags: [design, intermediate] + + - front: "Why does WAFER store Forth flags as -1/0 instead of 1/0 in comparisons?" + back: | + Forth 2012 standard requires: TRUE = -1 (all bits set), FALSE = 0. + WASM comparisons produce 0/1, so codegen must convert: + bool_to_forth_flag: 0 - result → -1 if true, 0 if false + This is a single i32.sub instruction (cheap). + tags: [design, intermediate] + +# ============================================================================ +# CATEGORY C: ForthVM Struct +# ============================================================================ +forthvm: + + - front: "What is `user_here` in ForthVM?" + back: "Pointer to next free address in WASM linear memory for user data (variables, CREATE'd words). Separate from dictionary.here() which tracks dictionary-internal allocation. Synced to SYSVAR_HERE (memory offset 12) before each evaluate call." + source: "crates/core/src/outer.rs:212" + tags: [forthvm, intermediate] + + - front: "What is `ir_bodies` in ForthVM?" + back: "HashMap> — stores the optimized IR body of every compiled word. Used by: (1) optimizer's inline pass to look up callee bodies, (2) CONSOLIDATE to recompile everything, (3) wafer build to export." + source: "crates/core/src/outer.rs:243" + tags: [forthvm, intermediate] + + - front: "What is the `control_stack` in ForthVM?" + back: "Vec — compile-time stack for nested control flow. IF pushes ControlEntry::If, DO pushes ControlEntry::Do, etc. THEN/LOOP/REPEAT pop and emit the corresponding IrOp. Not the runtime return stack — this is purely compile-time." + source: "crates/core/src/outer.rs:197" + tags: [forthvm, intermediate] + + - front: "What is `pending_actions` in ForthVM?" + back: "Arc>> — queue of actions from host functions that need compiler-side processing. Used by COMPILE, (CompileCall), CS-PICK, CS-ROLL, and POSTPONE of control-flow words. Processed after immediate word returns." + source: "crates/core/src/outer.rs:229" + tags: [forthvm, advanced] + + - front: "What is `pending_define` in ForthVM?" + back: "Arc>> — signals from host functions to the outer interpreter: 1=CONSTANT, 2=VARIABLE, 3=CREATE, 4=EVALUATE. Host function sets the code, outer interpreter reads it after execution and performs the action." + source: "crates/core/src/outer.rs:227" + tags: [forthvm, advanced] + + - front: "What does `does_definitions` store?" + back: "HashMap — for each DOES>-based defining word, stores: create_ir (code before DOES>), does_action_id (WordId of code after DOES>), has_create flag. Used when the defining word executes to set up new instances." + source: "crates/core/src/outer.rs:216" + tags: [forthvm, advanced] + + - front: "What happened to the `emit_func` field in ForthVM?" + back: "It moved into the Runtime implementation. NativeRuntime owns emit_func as a wasmtime::Func. WebRuntime creates it as a JS closure. ForthVM no longer directly holds wasmtime types — it only interacts via the Runtime trait." + source: "crates/core/src/runtime_native.rs:116" + tags: [forthvm, intermediate] + + - front: "What are `two_value_words` and `fvalue_words`?" + back: "HashSet tracking which word IDs are 2VALUEs or FVALUEs. TO needs to know: regular VALUE stores 1 cell, 2VALUE stores 2 cells, FVALUE stores 1 float (8 bytes). Without these sets, TO wouldn't know the storage semantics." + source: "crates/core/src/outer.rs:237-239" + tags: [forthvm, advanced] + + - front: "How many fields does ForthVM have? Name the major groups." + back: | + ~35 fields in 7 groups: + 1. Runtime: rt: R (generic — replaces old engine/store/memory/table/dsp/rsp/fsp/emit_func) + 2. Compilation: state, compiling_name, compiling_ir, control_stack, compiling_word_id, compiling_locals + 3. Output: output (Arc>) + 4. Dictionary bridge: dictionary, user_here, here_cell, base_cell + 5. Word metadata: ir_bodies, host_word_names, word_pfa_map, does_definitions + 6. Host function shared state: pending_define, pending_actions, pending_does_patch, throw_code, word_lookup + 7. Config + advanced: config, batch_mode, deferred_ir, marker_states, conditional_skip_depth, substitutions, search_order, next_wid, toplevel_ir + source: "crates/core/src/outer.rs:173-260" + tags: [forthvm, advanced] + +# ============================================================================ +# CATEGORY D: Codegen Details +# ============================================================================ +codegen: + + - front: "What WASM local index is the cached DSP?" + back: "Local 0 (CACHED_DSP_LOCAL). At function entry: global.get $dsp → local.set 0. All stack ops use local 0. Scratch locals start at SCRATCH_BASE = 1." + source: "crates/core/src/codegen.rs:58-61" + tags: [codegen, basic] + + - front: "What does `dsp_writeback` do and when is it called?" + back: "Writes the cached DSP local back to the $dsp global: `local.get 0; global.set $dsp`. Called before: (1) call_indirect/call (callee needs correct dsp), (2) function exit (return)." + code: | + fn dsp_writeback(f: &mut Function) { + f.instruction(&LocalGet(CACHED_DSP_LOCAL)) + .instruction(&GlobalSet(DSP)); + } + source: "crates/core/src/codegen.rs:167-173" + tags: [codegen, intermediate] + + - front: "How does codegen emit IrOp::Dup?" + back: | + Dup = peek top of stack, push copy: + 1. peek(f): local.get $dsp; i32.load (value now on WASM operand stack) + 2. push_via_local(f, SCRATCH_BASE): local.set $tmp; dsp_dec; local.get $dsp; local.get $tmp; i32.store + source: "crates/core/src/codegen.rs:359-362" + tags: [codegen, intermediate] + + - front: "How does codegen emit IrOp::Call(id)?" + back: | + 1. dsp_writeback (callee needs correct dsp) + 2. If id == self_word_id (self-recursion): emit direct `call WORD_FUNC` + 3. Else: i32.const fn_index; call_indirect (type_void, table 0) + 4. dsp_reload (callee may have changed dsp) + source: "crates/core/src/codegen.rs (emit_op Call branch)" + tags: [codegen, intermediate] + + - front: "What is EmitCtx and what fields does it have?" + back: | + Carries context for WASM code emission: + - f64_local_0, f64_local_1: scratch locals for float ops + - forth_local_base: base WASM local for Forth locals ({: ... :}) + - loop_local_base: base local for DO/LOOP index/limit pairs + - loop_locals: Vec<(index_local, limit_local)> stack for nested loops + - fast_loop_depth: nesting depth of loops using local fast path + - self_word_id: Option for self-recursion detection + - open_blocks: Vec for flat forward branches (CS-ROLL) + source: "crates/core/src/codegen.rs:229-250" + tags: [codegen, advanced] + + - front: "What are TYPE_VOID and TYPE_I32 in codegen?" + back: "Type section indices: TYPE_VOID=0 is () → () (used by most word functions and call_indirect), TYPE_I32=1 is (i32) → () (used by the emit import function)." + source: "crates/core/src/codegen.rs:44-45" + tags: [codegen, basic] + + - front: "How does the codegen handle DO/LOOP?" + back: | + Fast path: index and limit stored in WASM locals (no return stack). + - DO: pop limit and index from data stack into locals + - Loop body: I (RFetch) reads from index local + - LOOP: increment index local, compare with limit, br_if to loop start + - LEAVE: set SYSVAR_LEAVE_FLAG, break out of loop + Fallback: if loop is too complex, use return stack (rpush/rpop). + source: "crates/core/src/codegen.rs (DoLoop handling)" + tags: [codegen, advanced] + +# ============================================================================ +# CATEGORY E: Boot.fth +# ============================================================================ +boot_fth: + + - front: "What are the 7 phases of boot.fth?" + back: | + 1. Stack/memory: DEPTH, PICK, 2OVER, 2ROT, WITHIN, 2@, 2!, FILL, CMOVE, MOVE, ERASE, /STRING, -TRAILING + 2. Double-cell arithmetic: D+, DNEGATE, D-, DABS, D0=, D0<, D=, D<, D2*, D2/, DMAX, DMIN, M+, DU< + 3. Mixed arithmetic: SM/REM, FM/MOD, */, */MOD + 4. HERE and ALIGNED: HERE, ALLOT, comma, C-comma, ALIGN + 5. I/O + pictured numeric output: TYPE, SPACES, <# HOLD HOLDS SIGN # #S #> . U. .R U.R D. D.R + 6. DEFER support: DEFER!, DEFER@ + 7. String ops + misc: COMPARE, -TRAILING, SOURCE, FALIGNED, SFALIGNED, DFALIGNED + source: "crates/core/boot.fth" + tags: [boot, intermediate] + + - front: "How is DEPTH defined in boot.fth and why?" + back: | + : DEPTH SP@ 5632 SWAP - 2 RSHIFT ; + 5632 = DATA_STACK_TOP (0x1600). Stack grows down, so depth = (top - sp) / 4. + SP@ must come first — it reads dsp BEFORE DEPTH's own literal pushes affect it. + 2 RSHIFT = divide by 4 (arithmetic right shift, CELL_SIZE=4). + code: | + : DEPTH SP@ 5632 SWAP - 2 RSHIFT ; + source: "crates/core/boot.fth:12" + tags: [boot, intermediate] + + - front: "What magic numbers appear in boot.fth and what do they mean?" + back: | + 5632 (0x1600) = DATA_STACK_TOP + 1472 (0x05C0) = PICT_BUF_TOP (also WORD_BUF_BASE) + 12 = SYSVAR_HERE offset + 28 = SYSVAR_HLD offset + 64 = INPUT_BUFFER_BASE + 24 = SYSVAR_NUM_TIB offset + source: "crates/core/boot.fth" + tags: [boot, intermediate] + + - front: "How does pictured numeric output work in boot.fth?" + back: | + <# initializes HLD to PICT_BUF_TOP (1472) + HOLD decrements HLD and stores a character (grows downward) + # extracts one digit: divides ud by BASE via two UM/MODs, converts digit to ASCII, HOLDs it + #S calls # repeatedly until ud is zero + #> returns (c-addr u) pointing to the formatted string in the pictured buffer + SIGN adds '-' if the original number was negative + code: | + : <# 1472 28 ! ; + : HOLD 28 @ 1- DUP 28 ! C! ; + : # BASE @ >R 0 R@ UM/MOD R> SWAP >R UM/MOD + SWAP DUP 9 > IF 7 + THEN 48 + HOLD R> ; + source: "crates/core/boot.fth:193-224" + tags: [boot, advanced] + + - front: "Why is . (dot) defined in Forth instead of as a Rust host function?" + back: | + : . DUP ABS 0 <# #S ROT SIGN #> TYPE SPACE ; + 1. Self-hosting goal: Forth definitions > Rust + 2. Compiled Forth with inlining beats host function dispatch + 3. . becomes inlineable (IR body available to optimizer) + 4. Participates in CONSOLIDATE (direct calls in single module) + 5. Respects BASE correctly via pictured numeric output + source: "crates/core/boot.fth:228" + tags: [boot, advanced] + +# ============================================================================ +# CATEGORY F: Testing & CLI +# ============================================================================ +testing: + + - front: "What are eval_output and eval_stack test helpers?" + back: | + eval_output("forth code") → creates ForthVM, evaluates code, returns output String + eval_stack("forth code") → creates ForthVM, evaluates code, returns data stack as Vec + Both create a fresh VM for each test (isolated). + source: "crates/core/src/outer.rs (test module)" + tags: [testing, basic] + + - front: "How does the compliance test infrastructure work?" + back: | + 1. boot_with_prerequisites(): create VM, load tester.fr, core.fr, utilities.fth, coreexttest.fth + 2. run_suite(vm, file): reset #ERRORS to 0, load test file, read #ERRORS from data stack + 3. Assert #ERRORS == 0 for pass + 4. 11 word sets tested: Core, Core+, CoreExt, Double, Exception, Facility, File, Float, Locals, Memory, String + source: "crates/core/tests/compliance.rs" + tags: [testing, intermediate] + + - front: "What are the 4 ways to run WAFER?" + back: | + 1. `wafer` — interactive CLI REPL (rustyline, NativeRuntime) + 2. `wafer file.fth` — evaluate file and exit (NativeRuntime) + 3. `wafer build file.fth` — compile to .wasm or --native executable + 4. Browser REPL — wasm-pack build crates/web, serve www/, WebRuntime + Also: `wafer run file.wasm` — execute pre-compiled module + source: "crates/cli/src/main.rs:58-83" + tags: [cli, basic] + + - front: "How does the native executable trick work (wafer build --native)?" + back: | + 1. AOT-compile WASM via wasmtime Engine::precompile_module() + 2. Read current wafer binary + 3. Append: [wafer binary] + [precompiled payload] + [metadata JSON] + [24-byte trailer] + 4. Trailer: payload_len(8) + metadata_len(8) + "WAFEREXE"(8) + 5. On startup, check_embedded_payload() reads trailer, extracts payload, runs it + code: | + const NATIVE_MAGIC: &[u8; 8] = b"WAFEREXE"; + const TRAILER_SIZE: u64 = 24; + source: "crates/cli/src/main.rs:12-14" + tags: [cli, advanced] + +# ============================================================================ +# CATEGORY G: Control Flow Compilation +# ============================================================================ +control_flow: + + - front: "Name all 13 ControlEntry variants." + back: | + If, IfElse, Do, Begin, BeginWhile, BeginWhileWhile, + PostDoubleWhileRepeat, PostDoubleWhileRepeatElse, + Case, Of, QDo, Ahead, BeginRef, ForwardBlock + source: "crates/core/src/outer.rs:36-105" + tags: [control_flow, advanced] + + - front: "How does IF...ELSE...THEN compile?" + back: | + 1. IF: push ControlEntry::If { then_body: [] }; subsequent IR goes to then_body + 2. ELSE: pop If, push ControlEntry::IfElse { then_body, else_body: [] }; subsequent IR goes to else_body + 3. THEN: pop IfElse (or If), emit IrOp::If { then_body, else_body } + The IR is a tree — nested bodies, not flat branches. + tags: [control_flow, intermediate] + + - front: "How does DO...LOOP compile?" + back: | + 1. DO: push ControlEntry::Do { body: [] }; subsequent IR goes to body + 2. LOOP: pop Do, emit IrOp::DoLoop { body, is_plus_loop: false } + 3. +LOOP: same but is_plus_loop: true + The limit and index are expected on the data stack before the DoLoop executes. + tags: [control_flow, intermediate] + + - front: "How does BEGIN...WHILE...REPEAT compile?" + back: | + 1. BEGIN: push ControlEntry::Begin { body: [] } + 2. WHILE: pop Begin, push ControlEntry::BeginWhile { test: body, body: [] } + (everything before WHILE becomes the test) + 3. REPEAT: pop BeginWhile, emit IrOp::BeginWhileRepeat { test, body } + tags: [control_flow, intermediate] + + - front: "What is CASE...OF...ENDOF...ENDCASE compilation?" + back: | + 1. CASE: push ControlEntry::Case { prefix, endof_branches: [] } + 2. OF: duplicate test value, compare, pop Case, push ControlEntry::Of + 3. ENDOF: pop Of, save (test, body) pair, push back Case with new branch + 4. ENDCASE: pop Case, emit nested If chain from endof_branches + Desugared into nested IrOp::If at compile time. + tags: [control_flow, advanced] + + - front: "What are CS-PICK and CS-ROLL and why are they complex?" + back: | + Programming-Tools words that manipulate the compile-time control stack. + CS-PICK duplicates a control-flow entry N deep (e.g., reference a BEGIN from inside nested structures). + CS-ROLL rotates control-flow entries (e.g., move an IF dest across other structures). + Complex because they break the structured control-flow assumption — WAFER linearizes these into Block/BranchIfFalse/EndBlock IR ops for flat forward branches. + source: "crates/core/src/outer.rs:99-105" + tags: [control_flow, advanced] + +# ============================================================================ +# CATEGORY H: Consolidation & Export +# ============================================================================ +consolidation: + + - front: "What does compile_consolidated_module() produce?" + back: | + A single WASM module containing ALL compiled Forth words as separate functions. + - Each word gets a function index within the module + - Call(id) where id is in the module → direct `call N` (not call_indirect) + - Call(id) where id is NOT in the module → call_indirect (host functions) + - TailCall(id) in module → direct call + return + source: "crates/core/src/codegen.rs (compile_consolidated_module)" + tags: [consolidation, advanced] + + - front: "What metadata does wafer build embed in the .wasm file?" + back: | + ExportMetadata in a "wafer" custom section (JSON): + - version: 1 + - entry_table_index: Option + - host_functions: Vec<(table_index, name)> + - memory_size: u32 + - dsp_init, rsp_init, fsp_init: initial stack pointers + source: "crates/core/src/export.rs:21-36" + tags: [export, intermediate] + +# ============================================================================ +# CATEGORY I: Dictionary Details +# ============================================================================ +dictionary_details: + + - front: "What is the align4 function?" + back: "(addr + 3) & !3 — rounds up to next 4-byte boundary. Used to align the code field after variable-length name in dictionary entries." + code: | + fn align4(addr: u32) -> u32 { + (addr + 3) & !3 + } + source: "crates/core/src/dictionary.rs:51-53" + tags: [dictionary, basic] + + - front: "What is the hash index in Dictionary?" + back: | + HashMap> + Maps uppercase name → list of entries across wordlists. + find() checks search_order against this index (O(1) average). + Fallback: linked-list walk for words not yet in index. + Updated by reveal() and set_immediate(). + source: "crates/core/src/dictionary.rs:43" + tags: [dictionary, intermediate] + + - front: "What is DictionaryState and when is it used?" + back: | + Snapshot of dictionary state: latest, here, next_fn_index, index (HashMap clone). + Used by MARKER: save_state() captures current state, restore_state() reverts. + Does NOT save the actual memory bytes — just pointers and metadata. + source: "crates/core/src/dictionary.rs:502-509" + tags: [dictionary, intermediate] + + - front: "How does Dictionary::create() lay out an entry?" + back: | + Starting at self.here: + 1. Write link field (4 bytes): points to previous LATEST + 2. Write flags byte (1 byte): HIDDEN | length (optionally | IMMEDIATE) + 3. Write name bytes (N bytes, uppercase) + 4. Zero-pad to 4-byte alignment + 5. Write code field (4 bytes): next_fn_index (auto-incremented) + 6. Update latest = entry_start, here = after code field + code: | + // entry_start = self.here + // [link:4][flags:1][name:N][pad:0-3][code:4] + source: "crates/core/src/dictionary.rs:74-124" + tags: [dictionary, intermediate] + +# ============================================================================ +# CATEGORY J: Optimizer Patterns (hand-crafted supplements to auto-generated) +# ============================================================================ +optimizer_extra: + + - front: "What are the inline criteria?" + back: | + A Call(id) is inlined if ALL of: + 1. Body exists in `bodies` HashMap + 2. body.len() <= max_size (8) + 3. No self-recursion (contains_call_to check) + 4. No Exit (would return from caller) + 5. No ForthLocalGet/Set (would collide with caller's locals) + When inlined, TailCall ops are converted back to Call via detailcall(). + source: "crates/core/src/optimizer.rs:499-526" + tags: [optimizer, intermediate] + + - front: "How does tail_call_detect decide if a tail call is safe?" + back: | + 1. IR must be non-empty + 2. Return stack must be balanced: count ToR and FromR, depth must be 0 + (Unbalanced means >R without matching R>, which would corrupt return stack on tail call) + 3. convert_tail_call on last op: Call → TailCall + 4. Recurses into If branches: if last op is If, check both then/else branches + code: | + fn is_return_stack_balanced(ops: &[IrOp]) -> bool { + let mut depth: i32 = 0; + for op in ops { + match op { + IrOp::ToR => depth += 1, + IrOp::FromR => depth -= 1, + _ => {} + } + } + depth == 0 + } + source: "crates/core/src/optimizer.rs:671-693" + tags: [optimizer, advanced] + + - front: "What is the optimizer pass ordering and why does it matter?" + back: | + Phase 1 (simplify): peephole → fold → strength_reduce → peephole + Phase 2 (inline + re-simplify): inline → peephole → fold → strength_reduce → peephole + Phase 3 (eliminate): dce → peephole + Phase 4 (finalize): tail_call_detect + + Order matters because: + - Inline before fold: inlined body may have constant expressions + - Fold before strength: folding may produce power-of-2 constants + - DCE after fold: folded constants enable dead-branch elimination + - Tail call last: must operate on final IR + - Peephole between each: cleanup after every transformation + source: "crates/core/src/optimizer.rs:37-85" + tags: [optimizer, advanced] diff --git a/tools/anki_gen.py b/tools/anki_gen.py new file mode 100644 index 0000000..197e125 --- /dev/null +++ b/tools/anki_gen.py @@ -0,0 +1,687 @@ +#!/usr/bin/env python3 +""" +WAFER Anki Deck Generator + +Generates an Anki .apkg deck from: +1. Hand-crafted cards in anki_data.yaml +2. Auto-parsed IrOp variants from ir.rs +3. Auto-parsed memory constants from memory.rs +4. Auto-parsed error variants from error.rs +5. Auto-extracted peephole patterns from optimizer.rs + +Usage: + pip install genanki pyyaml + python tools/anki_gen.py + +Output: tools/wafer_anki.apkg +""" + +import hashlib +import re +import sys +from pathlib import Path + +try: + import genanki + import yaml +except ImportError: + print("Required: pip install genanki pyyaml") + sys.exit(1) + +# --------------------------------------------------------------------------- +# Paths +# --------------------------------------------------------------------------- +TOOLS_DIR = Path(__file__).parent +PROJECT_ROOT = TOOLS_DIR.parent +CORE_SRC = PROJECT_ROOT / "crates" / "core" / "src" +BOOT_FTH = PROJECT_ROOT / "crates" / "core" / "boot.fth" +YAML_FILE = TOOLS_DIR / "anki_data.yaml" +OUTPUT_FILE = TOOLS_DIR / "wafer_anki.apkg" + +# --------------------------------------------------------------------------- +# Stable IDs (genanki needs deterministic model/deck IDs) +# --------------------------------------------------------------------------- + + +def stable_id(name: str) -> int: + """Generate a stable integer ID from a name.""" + h = hashlib.md5(name.encode()).hexdigest() + return int(h[:8], 16) + + +DECK_ID = stable_id("wafer-learning-deck") +MODEL_ID = stable_id("wafer-card-model") + +# --------------------------------------------------------------------------- +# Anki model with code styling +# --------------------------------------------------------------------------- + +CSS = """\ +.card { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + font-size: 16px; + line-height: 1.5; + color: #1a1a1a; + background: #fafafa; + padding: 20px; + max-width: 700px; + margin: 0 auto; +} +.card.nightMode { + color: #e0e0e0; + background: #1e1e1e; +} +.front { font-size: 18px; font-weight: 600; } +pre, code { + font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; + font-size: 14px; + background: #f0f0f0; + border-radius: 4px; +} +.nightMode pre, .nightMode code { + background: #2d2d2d; +} +pre { + padding: 12px; + overflow-x: auto; + border: 1px solid #ddd; +} +.nightMode pre { border-color: #444; } +code { padding: 2px 5px; } +.source { + margin-top: 12px; + font-size: 12px; + color: #888; + font-style: italic; +} +.tags-line { + margin-top: 8px; + font-size: 11px; + color: #aaa; +} +.tags-line span { + background: #e8e8e8; + padding: 1px 6px; + border-radius: 3px; + margin-right: 4px; +} +.nightMode .tags-line span { background: #3a3a3a; } +""" + +FRONT_TEMPLATE = """\ +
{{Front}}
+""" + +BACK_TEMPLATE = """\ +
{{Front}}
+
+
{{Back}}
+{{#Code}} +
{{Code}}
+{{/Code}} +{{#Source}} +
{{Source}}
+{{/Source}} +""" + +wafer_model = genanki.Model( + MODEL_ID, + "WAFER Card", + fields=[ + {"name": "Front"}, + {"name": "Back"}, + {"name": "Code"}, + {"name": "Source"}, + ], + templates=[ + { + "name": "Card 1", + "qfmt": FRONT_TEMPLATE, + "afmt": BACK_TEMPLATE, + }, + ], + css=CSS, +) + +# --------------------------------------------------------------------------- +# Card generation helpers +# --------------------------------------------------------------------------- + + +def make_note(front: str, back: str, code: str = "", source: str = "", tags: list | None = None) -> genanki.Note: + """Create a genanki Note with stable GUID.""" + guid = genanki.guid_for(front) + note = genanki.Note( + model=wafer_model, + fields=[front, back, code, source], + tags=tags or [], + guid=guid, + ) + return note + + +def html_escape(text: str) -> str: + """Minimal HTML escaping for card content.""" + return text.replace("&", "&").replace("<", "<").replace(">", ">") + + +def format_back(text: str) -> str: + """Convert back text (may have newlines) to HTML.""" + lines = text.strip().split("\n") + return "
".join(html_escape(line) for line in lines) + + +def format_code(text: str) -> str: + """Format code block content.""" + return html_escape(text.strip()) + + +# --------------------------------------------------------------------------- +# YAML card loader +# --------------------------------------------------------------------------- + + +def load_yaml_cards(deck: genanki.Deck) -> int: + """Load hand-crafted cards from anki_data.yaml.""" + if not YAML_FILE.exists(): + print(f"Warning: {YAML_FILE} not found, skipping hand-crafted cards") + return 0 + + with open(YAML_FILE) as f: + data = yaml.safe_load(f) + + count = 0 + for category, cards in data.items(): + if not isinstance(cards, list): + continue + for card in cards: + front = card.get("front", "") + back = card.get("back", "") + code = card.get("code", "") + source = card.get("source", "") + tags = card.get("tags", []) + + # Add category as tag + all_tags = [f"wafer::{category}"] + [f"wafer::{t}" for t in tags] + + note = make_note( + front=html_escape(front), + back=format_back(back), + code=format_code(code) if code else "", + source=html_escape(source), + tags=all_tags, + ) + deck.add_note(note) + count += 1 + + return count + + +# --------------------------------------------------------------------------- +# Auto-parse IrOp variants from ir.rs +# --------------------------------------------------------------------------- + + +def parse_ir_ops(deck: genanki.Deck) -> int: + """Parse IrOp enum from ir.rs and generate cards.""" + ir_file = CORE_SRC / "ir.rs" + if not ir_file.exists(): + return 0 + + content = ir_file.read_text() + count = 0 + + # Match doc comments + variant lines + # Pattern: /// comment\n VariantName or VariantName(type) or VariantName { ... } + lines = content.split("\n") + i = 0 + current_category = "" + + while i < len(lines): + line = lines[i].strip() + + # Track categories from // -- Category -- comments + cat_match = re.match(r"//\s*--\s*(.+?)\s*--", line) + if cat_match: + current_category = cat_match.group(1).strip() + i += 1 + continue + + # Collect doc comments + doc_lines = [] + while i < len(lines) and lines[i].strip().startswith("///"): + doc_lines.append(lines[i].strip().lstrip("/ ").strip()) + i += 1 + + if i >= len(lines): + break + + line = lines[i].strip() + + # Match variant definition + variant_match = re.match( + r"^((?:[A-Z][a-zA-Z0-9]+)(?:\([^)]*\))?)\s*[,{]", line + ) + if variant_match and doc_lines: + variant = variant_match.group(1) + # Clean up: remove trailing comma + variant = variant.rstrip(",") + doc = " ".join(doc_lines) + + # Extract stack effect if present: ( ... -- ... ) + stack_match = re.search(r"\(\s*(.+?)\s*\)", doc) + stack_effect = stack_match.group(0) if stack_match else "" + + front = f"IrOp::{variant} — what does it do?" + back_parts = [doc] + if stack_effect: + back_parts.insert(0, f"Stack: {stack_effect}") + back = "\n".join(back_parts) + + tags = ["wafer::ir", f"wafer::ir_{current_category.lower().replace(' ', '_')}"] + + note = make_note( + front=html_escape(front), + back=format_back(back), + code=format_code(f"IrOp::{variant}"), + source=f"crates/core/src/ir.rs", + tags=tags, + ) + deck.add_note(note) + count += 1 + + i += 1 + + return count + + +# --------------------------------------------------------------------------- +# Auto-parse memory constants from memory.rs +# --------------------------------------------------------------------------- + + +def parse_memory_constants(deck: genanki.Deck) -> int: + """Parse constants from memory.rs and generate cards.""" + mem_file = CORE_SRC / "memory.rs" + if not mem_file.exists(): + return 0 + + content = mem_file.read_text() + count = 0 + + # Match: /// doc comment\n pub const NAME: type = value; + lines = content.split("\n") + i = 0 + + while i < len(lines): + # Collect doc comments + doc_lines = [] + while i < len(lines) and lines[i].strip().startswith("///"): + doc_lines.append(lines[i].strip().lstrip("/ ").strip()) + i += 1 + + if i >= len(lines): + break + + line = lines[i].strip() + const_match = re.match( + r"pub const (\w+):\s*\w+\s*=\s*(.+?);", line + ) + if const_match and doc_lines: + name = const_match.group(1) + value_expr = const_match.group(2).strip() + doc = " ".join(doc_lines) + + # Try to evaluate simple expressions for the card + # (won't work for all, but catches most) + front = f"memory.rs: What is {name}?" + back = f"{doc}\nValue: {value_expr}" + + note = make_note( + front=html_escape(front), + back=format_back(back), + code=format_code(f"pub const {name}: u32 = {value_expr};"), + source="crates/core/src/memory.rs", + tags=["wafer::memory", "wafer::constants"], + ) + deck.add_note(note) + count += 1 + + # Also generate reverse card for address-based constants + if name.endswith("_BASE") or name.endswith("_TOP"): + # Try to find hex value + try: + val = eval(value_expr.replace("SYSVAR_BASE + ", "0 + ").replace("SYSVAR_BASE", "0")) + except Exception: + val = None + if isinstance(val, int): + rev_front = f"memory.rs: What region starts at 0x{val:04X}?" + rev_back = f"{name}: {doc}" + rev_note = make_note( + front=html_escape(rev_front), + back=format_back(rev_back), + source="crates/core/src/memory.rs", + tags=["wafer::memory", "wafer::constants", "wafer::reverse"], + ) + deck.add_note(rev_note) + count += 1 + + i += 1 + + return count + + +# --------------------------------------------------------------------------- +# Auto-parse error variants from error.rs +# --------------------------------------------------------------------------- + + +def parse_errors(deck: genanki.Deck) -> int: + """Parse WaferError enum from error.rs and generate cards.""" + err_file = CORE_SRC / "error.rs" + if not err_file.exists(): + return 0 + + content = err_file.read_text() + count = 0 + + # Match #[error("...")] followed by variant + pattern = re.compile(r'#\[error\("(.+?)"\)\]\s*\n\s*(\w+)(?:\((.+?)\))?', re.MULTILINE) + for m in pattern.finditer(content): + msg = m.group(1) + variant = m.group(2) + inner = m.group(3) or "" + + front = f"WaferError::{variant} — when is this error raised?" + back = f'Error message: "{msg}"' + if inner: + back += f"\nContains: {inner}" + + note = make_note( + front=html_escape(front), + back=format_back(back), + code=format_code(f"WaferError::{variant}"), + source="crates/core/src/error.rs", + tags=["wafer::error"], + ) + deck.add_note(note) + count += 1 + + return count + + +# --------------------------------------------------------------------------- +# Auto-extract peephole patterns from optimizer.rs +# --------------------------------------------------------------------------- + + +def parse_peephole_patterns(deck: genanki.Deck) -> int: + """Extract peephole optimization patterns from optimizer.rs.""" + opt_file = CORE_SRC / "optimizer.rs" + if not opt_file.exists(): + return 0 + + content = opt_file.read_text() + count = 0 + + # Match comment + pattern in peephole_one_pass + # Pattern: // Comment\n (IrOp::X, IrOp::Y) => { ... } + lines = content.split("\n") + in_peephole = False + i = 0 + + while i < len(lines): + line = lines[i].strip() + + if "fn peephole_one_pass" in line: + in_peephole = True + elif in_peephole and line.startswith("fn "): + in_peephole = False + + if in_peephole: + # Match pattern comments like: // PushI32(n), Drop => remove both + comment_match = re.match(r"//\s*(.+?)\s*=>\s*(.+)", line) + if comment_match: + pattern = comment_match.group(1).strip() + result = comment_match.group(2).strip() + + front = f"Peephole: {pattern} → ?" + back = result + + note = make_note( + front=html_escape(front), + back=format_back(back), + source="crates/core/src/optimizer.rs", + tags=["wafer::optimizer", "wafer::peephole"], + ) + deck.add_note(note) + count += 1 + + i += 1 + + return count + + +# --------------------------------------------------------------------------- +# Auto-generate primitive registration cards +# --------------------------------------------------------------------------- + + +def parse_primitives(deck: genanki.Deck) -> int: + """Extract IR primitive registrations from outer.rs.""" + outer_file = CORE_SRC / "outer.rs" + if not outer_file.exists(): + return 0 + + content = outer_file.read_text() + count = 0 + + # Match: self.register_primitive("NAME", false, vec![IrOp::X, IrOp::Y])?; + pattern = re.compile( + r'self\.register_primitive\("(.+?)",\s*(true|false),\s*vec!\[(.+?)\]\)', + re.DOTALL, + ) + + for m in pattern.finditer(content): + name = m.group(1) + immediate = m.group(2) == "true" + ir_body = m.group(3).strip() + # Clean up multiline + ir_body = " ".join(ir_body.split()) + + front = f"Forth word {name} — what is its IR body?" + back = f"IR: [{ir_body}]" + if immediate: + back += "\n(IMMEDIATE word)" + + note = make_note( + front=html_escape(front), + back=format_back(back), + code=format_code(f'register_primitive("{name}", {immediate}, vec![{ir_body}])'), + source="crates/core/src/outer.rs", + tags=["wafer::primitives", "wafer::ir"], + ) + deck.add_note(note) + count += 1 + + return count + + +# --------------------------------------------------------------------------- +# Auto-generate boot.fth definition cards +# --------------------------------------------------------------------------- + + +def parse_boot_fth(deck: genanki.Deck) -> int: + """Extract Forth definitions from boot.fth.""" + if not BOOT_FTH.exists(): + return 0 + + content = BOOT_FTH.read_text() + count = 0 + + lines = content.split("\n") + i = 0 + current_comment = "" + + while i < len(lines): + line = lines[i] + stripped = line.strip() + + # Track section comments + if stripped.startswith("\\") and not stripped.startswith("\\ -------"): + comment = stripped.lstrip("\\ ").strip() + if comment: + current_comment = comment + + # Match colon definitions + if stripped.startswith(": "): + # Collect full definition (may span multiple lines) + defn = stripped + while not defn.rstrip().endswith(";") and i + 1 < len(lines): + i += 1 + defn += " " + lines[i].strip() + + # Extract name + name_match = re.match(r":\s+(\S+)", defn) + if name_match: + name = name_match.group(1) + + front = f"boot.fth: How is {name} defined?" + back = current_comment if current_comment else f"Forth definition of {name}" + + note = make_note( + front=html_escape(front), + back=format_back(back), + code=format_code(defn), + source="crates/core/boot.fth", + tags=["wafer::boot_fth"], + ) + deck.add_note(note) + count += 1 + + i += 1 + + return count + + +# --------------------------------------------------------------------------- +# Auto-parse Runtime trait methods from runtime.rs +# --------------------------------------------------------------------------- + + +def parse_runtime_trait(deck: genanki.Deck) -> int: + """Parse Runtime and HostAccess trait methods from runtime.rs.""" + rt_file = CORE_SRC / "runtime.rs" + if not rt_file.exists(): + return 0 + + content = rt_file.read_text() + count = 0 + + # Match trait method signatures with doc comments + lines = content.split("\n") + i = 0 + current_trait = "" + + while i < len(lines): + line = lines[i].strip() + + # Track which trait we're in + trait_match = re.match(r"(?:pub\s+)?trait (\w+)", line) + if trait_match: + current_trait = trait_match.group(1) + i += 1 + continue + + # Collect doc comments + doc_lines = [] + while i < len(lines) and lines[i].strip().startswith("///"): + doc_lines.append(lines[i].strip().lstrip("/ ").strip()) + i += 1 + + if i >= len(lines): + break + + line = lines[i].strip() + + # Check if this is a trait definition (may follow doc comments) + trait_match = re.match(r"(?:pub\s+)?trait (\w+)", line) + if trait_match: + current_trait = trait_match.group(1) + i += 1 + continue + + # Match fn signatures + fn_match = re.match(r"fn (\w+)\(", line) + if fn_match and doc_lines and current_trait: + fn_name = fn_match.group(1) + doc = " ".join(doc_lines) + + front = f"{current_trait}::{fn_name}() — what does it do?" + back = doc + + note = make_note( + front=html_escape(front), + back=format_back(back), + code=format_code(line.rstrip(";")), + source="crates/core/src/runtime.rs", + tags=["wafer::runtime", f"wafer::{current_trait.lower()}"], + ) + deck.add_note(note) + count += 1 + + i += 1 + + return count + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + + +def main() -> None: + """Generate the Anki deck.""" + deck = genanki.Deck(DECK_ID, "WAFER - WebAssembly Forth Engine in Rust") + + print("Generating WAFER Anki deck...") + print() + + # Load hand-crafted cards + n = load_yaml_cards(deck) + print(f" Hand-crafted cards (YAML): {n}") + + # Auto-generate from source + n = parse_ir_ops(deck) + print(f" IrOp variant cards: {n}") + + n = parse_memory_constants(deck) + print(f" Memory constant cards: {n}") + + n = parse_errors(deck) + print(f" Error variant cards: {n}") + + n = parse_peephole_patterns(deck) + print(f" Peephole pattern cards: {n}") + + n = parse_primitives(deck) + print(f" Primitive registration cards: {n}") + + n = parse_boot_fth(deck) + print(f" boot.fth definition cards: {n}") + + n = parse_runtime_trait(deck) + print(f" Runtime trait method cards: {n}") + + total = len(deck.notes) + print(f"\n TOTAL: {total} cards") + + # Write .apkg + genanki.Package(deck).write_to_file(str(OUTPUT_FILE)) + print(f"\nWrote {OUTPUT_FILE}") + print(f"Import into Anki: File > Import > select {OUTPUT_FILE.name}") + + +if __name__ == "__main__": + main() diff --git a/tools/architecture.txt b/tools/architecture.txt new file mode 100644 index 0000000..1354593 --- /dev/null +++ b/tools/architecture.txt @@ -0,0 +1,306 @@ +WAFER Architecture Reference (updated 2026-04-13) +=================================================== + +1. COMPILATION PIPELINE +----------------------- + + Forth Source + | + v + Outer Interpreter (outer.rs) + +--------------------------------------------+ + | Tokenizer: whitespace-delimited words | + | For each token: | + | 1. Dictionary lookup (find) | + | 2. If found + interpret mode: EXECUTE | + | 3. If found + compile mode: | + | - Immediate? Execute now | + | - Normal? Append Call(WordId) to IR | + | 4. Not found: try parse as number | + | - Interpret: push to data stack | + | - Compile: append PushI32(n) to IR | + | 5. Neither: error "unknown word" | + +--------------------------------------------+ + | On `;` (end of colon definition): + v + Optimizer (optimizer.rs) + +--------------------------------------------+ + | Phase 1: Simplify | + | Peephole -> Constant Fold -> | + | Strength Reduce -> Peephole | + | Phase 2: Inline then re-simplify | + | Inline(max=8) -> Peephole -> | + | Constant Fold -> Strength Reduce -> | + | Peephole | + | Phase 3: Eliminate dead code | + | DCE -> Peephole | + | Phase 4: Tail calls (must be last) | + | Tail Call Detect | + +--------------------------------------------+ + | + v + Codegen (codegen.rs) + +--------------------------------------------+ + | IR -> WASM bytecode via wasm-encoder | + | Each word = one WASM module with: | + | Imports: emit, memory, dsp, rsp, fsp, | + | table | + | Types: void () -> (), i32 (i32) -> () | + | One defined function (the word body) | + | DSP cached in local 0, writeback before | + | calls, reload after calls | + | Scratch locals start at index 1 | + +--------------------------------------------+ + | + v + Runtime trait (runtime.rs) + +--------------------------------------------+ + | ForthVM — generic over backend | + | Runtime provides: | + | - Memory r/w (mem_read_i32, etc.) | + | - Globals (get/set_dsp, rsp, fsp) | + | - Table (ensure_table_size) | + | - instantiate_and_install(wasm_bytes) | + | - call_func(fn_index) | + | - register_host_func(fn_index, HostFn) | + | | + | HostAccess trait — memory/global ops for | + | host function callbacks | + | HostFn = Box | + +--------------------------------------------+ + | | + v v + NativeRuntime WebRuntime + (runtime_native.rs) (crates/web/runtime_web.rs) + +------------------+ +------------------+ + | wasmtime Engine | | js_sys::WebAsm | + | Store, Memory | | Memory, Table | + | Table, Globals | | Global objects | + | Func closures | | JS Closures | + +------------------+ +------------------+ + + +2. MEMORY LAYOUT (Linear Memory) +-------------------------------- + + Address Region Size Notes + -------- ------------------ ------- ------------------------- + 0x0000 System Variables 64 B STATE, BASE, >IN, HERE, + LATEST, SOURCE-ID, #TIB, + HLD, LEAVE-FLAG + 0x0040 Input Buffer 1024 B Source parsing + 0x0440 PAD 256 B Scratch area + 0x0540 Pictured Output 128 B <# ... #> (grows down) + 0x05C0 WORD Buffer 64 B Transient counted string + 0x0600 Data Stack 4096 B 1024 cells, grows DOWN + 0x1600 (Data Stack Top) DSP starts here + 0x1540 Return Stack 4096 B Grows DOWN + 0x2540 Float Stack 2048 B 256 doubles, grows DOWN + 0x2D40 Dictionary grows UP Linked list of word entries + + Total initial memory: 16 pages = 1 MiB (max 256 pages = 16 MiB) + Cell size: 4 bytes (i32) + Float size: 8 bytes (f64) + + +3. SYSTEM VARIABLES (offsets from 0x0000) +----------------------------------------- + + Offset Name Purpose + ------ ---------- ----------------------------------- + 0 STATE 0=interpreting, -1=compiling + 4 BASE Number base (default 10) + 8 >IN Parse offset into input buffer + 12 HERE Next free dictionary address + 16 LATEST Most recent dictionary entry addr + 20 SOURCE-ID 0=user input, -1=string + 24 #TIB Length of current input + 28 HLD Pictured numeric output pointer + 32 LEAVE-FLAG Nonzero when LEAVE called in loop + + +4. DICTIONARY ENTRY FORMAT +-------------------------- + + +--------+-------+----------+---------+-----------+ + | Link | Flags | Name | Padding | Code | + | 4 bytes| 1 byte| N bytes | 0-3 B | 4 bytes | + +--------+-------+----------+---------+-----------+ + ^ ^ + entry_addr code field (fn table index) + + Flags byte: + Bit 7 (0x80): IMMEDIATE + Bit 6 (0x40): HIDDEN (during compilation) + Bits 0-4 (0x1F): name length (max 31) + + Link points to previous entry (0 = end of list). + Name stored uppercase, padded to 4-byte alignment. + Code field: index into WASM function table. + Parameter field (if any) follows immediately after code field. + + +5. THREE TYPES OF WORDS +----------------------- + + a) IR Primitives (compiled to WASM) + register_primitive("DUP", false, vec![IrOp::Dup]) + - Body stored as Vec + - Optimized, then compiled to WASM module + - Inlineable by optimizer + - FAST: no function call overhead when inlined + + b) Host Functions (HostFn closures) + register_host_primitive(".", false, func) + - HostFn = Box Result<()>> + - Access memory/globals via HostAccess trait (runtime-agnostic) + - NOT inlineable + - Used for: I/O, dictionary manipulation, complex logic + - Same closure works on NativeRuntime and WebRuntime + + c) Forth-defined words + : SQUARE DUP * ; + - Compiled by outer interpreter + - Goes through full optimize -> codegen pipeline + - Stored in ir_bodies for future inlining + + +6. WASM MODULE STRUCTURE (per word) +----------------------------------- + + Imports (6) — provided by Runtime impl: + 0. emit (func: i32 -> void) Character output callback + 1. memory (memory: 16 pages) Shared linear memory + 2. dsp (global: mut i32) Data stack pointer + 3. rsp (global: mut i32) Return stack pointer + 4. fsp (global: mut i32) Float stack pointer + 5. table (table: funcref) Shared function table + + Types (2): + 0. void: () -> () + 1. i32: (i32) -> () + + Functions (1): + The compiled word body + + Element section: + table[base_fn_index] = function 1 + + Runtime::instantiate_and_install(wasm_bytes, fn_index): + - NativeRuntime: Module::new + Instance::new with 6 wasmtime imports + - WebRuntime: WebAssembly.instantiate with JS import objects + + +7. OPTIMIZATION PASSES (detail) +------------------------------- + + PEEPHOLE (runs 5x across full pipeline): + PushI32(n), Drop -> (removed) Unused literal + Dup, Drop -> (removed) Redundant copy + Swap, Swap -> (removed) Self-inverse + Swap, Drop -> Nip Combine + PushI32(0), Add -> (removed) Identity + PushI32(0), Or -> (removed) Identity + PushI32(-1), And -> (removed) Identity + PushI32(1), Mul -> (removed) Identity + Over, Over -> TwoDup Combine + Drop, Drop -> TwoDrop Combine + (+ float variants: PushF64/FDrop, FDup/FDrop, FSwap/FSwap, FNegate/FNegate) + + CONSTANT FOLD: + Binary: PushI32(a), PushI32(b), -> PushI32(result) + Supports: Add, Sub, Mul, And, Or, Xor, Lshift, Rshift, ArithRshift, + Eq, NotEq, Lt, Gt, LtUnsigned + Unary: PushI32(n), -> PushI32(result) + Supports: Negate, Abs, Invert, ZeroEq, ZeroLt + Float binary: PushF64(a), PushF64(b), -> PushF64(result) + Float unary: PushF64(n), -> PushF64(result) + + STRENGTH REDUCE: + PushI32(2^n), Mul -> PushI32(n), Lshift + PushI32(0), Eq -> ZeroEq + PushI32(0), Lt -> ZeroLt + + DCE: + PushI32(nonzero), If{then,else} -> then_body only + PushI32(0), If{then,else} -> else_body only + Everything after Exit -> removed + + INLINE (max_size=8, single pass): + Call(id) -> inline body if: + - Body length <= 8 ops + - No self-recursion + - No Exit (would return from caller) + - No ForthLocalGet/Set (would collide with caller's locals) + TailCall -> Call when inlined (no longer tail position) + + TAIL CALL (last pass): + Last Call(id) -> TailCall(id) if: + - Return stack balanced (equal ToR and FromR) + Recurses into If branches for conditional tail calls + + +8. CONSOLIDATION +---------------- + + CONSOLIDATE word recompiles all JIT-compiled words into a + single WASM module: + - All call_indirect -> direct call (for words in module) + - External calls (host functions) remain call_indirect + - Maximum performance for final program + + Two-part implementation: + codegen::compile_consolidated_module() - builds multi-function module + outer::ForthVM::consolidate() - orchestrates collection + table update + + +9. EXPORT PIPELINE (wafer build) +-------------------------------- + + 1. Evaluate source file with recording_toplevel=true + 2. Collect all IR words + top-level IR + 3. Determine entry: --entry flag > MAIN word > top-level execution + 4. Build consolidated module with data section (memory snapshot) + 5. Embed metadata in "wafer" custom section (JSON) + 6. Optional: --js generates JS loader + HTML page + 7. Optional: --native AOT-compiles and appends to wafer binary + Format: [wafer binary][precompiled WASM][metadata][trailer] + Trailer: payload_len(8) + metadata_len(8) + "WAFEREXE"(8) + + +10. CRATE STRUCTURE +------------------- + + crates/ + core/ wafer-core: compiler, optimizer, codegen, dictionary, Runtime trait + Feature flags: default=["native"], "native" enables wasmtime + Without features: pure Rust (dictionary, IR, optimizer, codegen, outer) + cli/ wafer: CLI REPL (rustyline), wafer build/run commands + web/ wafer-web: browser REPL (wasm-bindgen + WebRuntime + HTML/CSS/JS) + + Key web files: + crates/web/src/lib.rs WaferRepl wasm-bindgen entry point + crates/web/src/runtime_web.rs WebRuntime: js_sys WebAssembly API + crates/web/www/app.js Frontend JS (terminal emulation) + crates/web/www/index.html HTML shell + crates/web/www/style.css Styling + + +11. BOOT SEQUENCE +----------------- + + ForthVM::::new() -> + 1. R::new() — create runtime (wasmtime or browser WASM) + 2. register_primitives() in batch_mode: + - ~40 IR primitives (DUP, +, @, etc.) + - ~60 host functions (., .S, M*, ACCEPT, etc.) + - ~30 special words (IF, DO, :, VARIABLE, etc.) + 3. compile_batch() - single WASM module for all IR primitives + 4. Load boot.fth - Forth replaces Rust host functions: + Phase 1: Stack/memory (DEPTH, PICK, 2OVER, FILL, MOVE) + Phase 2: Double-cell arithmetic (D+, DNEGATE, D<) + Phase 3: Mixed arithmetic (SM/REM, FM/MOD, */, */MOD) + Phase 4: HERE, ALLOT, comma, ALIGN + Phase 5: I/O, pictured numeric output (., U., TYPE, <# # #>) + Phase 6: DEFER support + Phase 7: String operations (COMPARE, SOURCE, FALIGNED) diff --git a/tools/ir_quiz.py b/tools/ir_quiz.py new file mode 100644 index 0000000..dbdf96d --- /dev/null +++ b/tools/ir_quiz.py @@ -0,0 +1,347 @@ +#!/usr/bin/env python3 +"""WAFER IR Flash Quiz — predict the optimized IR for Forth code.""" + +import random +import sys + +# Each exercise: (forth_code, accepted_answers, explanation) +# accepted_answers: list of strings that count as correct (case-insensitive, whitespace-normalized) +EXERCISES = [ + # --- Constant Folding --- + ( + ": FOO 2 3 + ;", + ["PushI32(5)", "pushi32(5)", "5"], + "Constant fold: PushI32(2), PushI32(3), Add → PushI32(5)", + ), + ( + ": FOO 10 3 - ;", + ["PushI32(7)", "pushi32(7)", "7"], + "Constant fold: PushI32(10), PushI32(3), Sub → PushI32(7)", + ), + ( + ": FOO 6 7 * ;", + ["PushI32(42)", "pushi32(42)", "42"], + "Constant fold: PushI32(6), PushI32(7), Mul → PushI32(42)", + ), + ( + ": FOO 5 0= ;", + ["PushI32(0)", "pushi32(0)", "0", "false"], + "Constant fold (unary): PushI32(5), ZeroEq → PushI32(0) (5 is not zero)", + ), + ( + ": FOO 0 0= ;", + ["PushI32(-1)", "pushi32(-1)", "-1", "true"], + "Constant fold (unary): PushI32(0), ZeroEq → PushI32(-1) (true flag)", + ), + ( + ": FOO -3 ABS ;", + ["PushI32(3)", "pushi32(3)", "3"], + "Constant fold (unary): PushI32(-3), Abs → PushI32(3)", + ), + ( + ": FOO 255 INVERT ;", + ["PushI32(-256)", "pushi32(-256)", "-256"], + "Constant fold (unary): PushI32(255), Invert → PushI32(-256) (bitwise NOT)", + ), + ( + ": FOO 3 2 LSHIFT ;", + ["PushI32(12)", "pushi32(12)", "12"], + "Constant fold: PushI32(3), PushI32(2), Lshift → PushI32(12) (3 << 2 = 12)", + ), + + # --- Peephole --- + ( + ": FOO DUP DROP ;", + ["(empty)", "empty", "nothing", "[]", ""], + "Peephole: Dup, Drop → removed (both eliminated)", + ), + ( + ": FOO SWAP SWAP ;", + ["(empty)", "empty", "nothing", "[]", ""], + "Peephole: Swap, Swap → removed (self-inverse)", + ), + ( + ": FOO SWAP DROP ;", + ["Nip", "nip"], + "Peephole: Swap, Drop → Nip", + ), + ( + ": FOO DROP DROP ;", + ["TwoDrop", "twodrop", "2drop"], + "Peephole: Drop, Drop → TwoDrop", + ), + ( + ": FOO OVER OVER ;", + ["TwoDup", "twodup", "2dup"], + "Peephole: Over, Over → TwoDup", + ), + ( + ": FOO 0 + ;", + ["(empty)", "empty", "nothing", "[]", ""], + "Peephole: PushI32(0), Add → removed (identity)", + ), + ( + ": FOO 1 * ;", + ["(empty)", "empty", "nothing", "[]", ""], + "Peephole: PushI32(1), Mul → removed (identity)", + ), + ( + ": FOO -1 AND ;", + ["(empty)", "empty", "nothing", "[]", ""], + "Peephole: PushI32(-1), And → removed (identity, all bits set)", + ), + ( + ": FOO 0 OR ;", + ["(empty)", "empty", "nothing", "[]", ""], + "Peephole: PushI32(0), Or → removed (identity)", + ), + ( + ": FOO 42 DROP ;", + ["(empty)", "empty", "nothing", "[]", ""], + "Peephole: PushI32(42), Drop → removed (unused literal)", + ), + + # --- Strength Reduction --- + ( + ": FOO 8 * ;", + ["PushI32(3), Lshift", "pushi32(3) lshift", "3 lshift"], + "Strength reduce: PushI32(8) is 2^3, Mul → PushI32(3), Lshift", + ), + ( + ": FOO 16 * ;", + ["PushI32(4), Lshift", "pushi32(4) lshift", "4 lshift"], + "Strength reduce: PushI32(16) is 2^4, Mul → PushI32(4), Lshift", + ), + ( + ": FOO 2 * ;", + ["PushI32(1), Lshift", "pushi32(1) lshift", "1 lshift"], + "Strength reduce: PushI32(2) is 2^1, Mul → PushI32(1), Lshift", + ), + ( + ": FOO 0 = ;", + ["ZeroEq", "zeroeq", "0="], + "Strength reduce: PushI32(0), Eq → ZeroEq", + ), + ( + ": FOO 0 < ;", + ["ZeroLt", "zerolt", "0<"], + "Strength reduce: PushI32(0), Lt → ZeroLt", + ), + + # --- Dead Code Elimination --- + ( + ": FOO TRUE IF 42 ELSE 99 THEN ;", + ["PushI32(42)", "pushi32(42)", "42"], + "DCE: PushI32(-1) is nonzero → then_body only → PushI32(42)", + ), + ( + ": FOO FALSE IF 42 ELSE 99 THEN ;", + ["PushI32(99)", "pushi32(99)", "99"], + "DCE: PushI32(0) is zero → else_body only → PushI32(99)", + ), + ( + ": FOO EXIT 42 ;", + ["Exit", "exit"], + "DCE: Everything after Exit is removed. PushI32(42) eliminated.", + ), + + # --- Combined Optimizations --- + ( + ": FOO DUP * ;", + ["Dup, Mul", "dup mul", "dup, mul"], + "Inline DUP and *: [Dup, Mul]. No further optimizations apply.", + ), + ( + ": FOO 2 3 + 4 * ;", + ["PushI32(20)", "pushi32(20)", "20"], + "Fold 2+3=5, then fold 5*4=20. Single constant.", + ), + ( + ": FOO 1 2 + 8 * ;", + ["PushI32(24)", "pushi32(24)", "24"], + "Fold 1+2=3, strength reduce 8*? No — fold first: 3*8=24.", + ), + ( + ": FOO 0 0 + ;", + ["PushI32(0)", "pushi32(0)", "0"], + "Fold: PushI32(0), PushI32(0), Add → PushI32(0)", + ), + ( + ": FOO SWAP DUP DROP SWAP ;", + ["(empty)", "empty", "nothing", "[]", ""], + "Peephole chain: Swap,Dup → ...; Dup,Drop → removed; Swap,Swap → removed. All gone.", + ), + + # --- Inlining --- + ( + ": SQUARE DUP * ;\n: FOO SQUARE ;", + ["Dup, Mul", "dup mul", "dup, mul"], + "SQUARE body=[Dup,Mul] (2 ops ≤ 8). Inlined into FOO. Tail call: Dup is not Call, Mul is not Call → no tail call.", + ), + + # --- Tail Call --- + ( + ": BAR 1 ; : FOO 42 BAR ;", + ["PushI32(42), TailCall(bar_id)", "pushi32(42) tailcall", "42 tailcall(bar)"], + "BAR has body [PushI32(1)] — 1 op, inlineable. But wait: if BAR is inlined, result is [PushI32(42), PushI32(1)]. Actually depends on whether BAR is inlined. If NOT inlined: tail call applies to Call(bar). If inlined: [PushI32(42), PushI32(1)].", + ), + + # --- Float --- + ( + ": FOO 1.0E0 2.0E0 F+ ;", + ["PushF64(3.0)", "pushf64(3.0)", "3.0"], + "Float constant fold: PushF64(1.0), PushF64(2.0), FAdd → PushF64(3.0)", + ), + ( + ": FOO -5.0E0 FABS ;", + ["PushF64(5.0)", "pushf64(5.0)", "5.0"], + "Float unary fold: PushF64(-5.0), FAbs → PushF64(5.0)", + ), + ( + ": FOO FNEGATE FNEGATE ;", + ["(empty)", "empty", "nothing", "[]", ""], + "Peephole: FNegate, FNegate → removed (self-inverse)", + ), + ( + ": FOO FSWAP FSWAP ;", + ["(empty)", "empty", "nothing", "[]", ""], + "Peephole: FSwap, FSwap → removed (self-inverse)", + ), + ( + ": FOO FDUP FDROP ;", + ["(empty)", "empty", "nothing", "[]", ""], + "Peephole: FDup, FDrop → removed", + ), + + # --- Tricky --- + ( + ": FOO 3 5 < ;", + ["PushI32(-1)", "pushi32(-1)", "-1", "true"], + "Constant fold: PushI32(3), PushI32(5), Lt → PushI32(-1) (3 < 5 is true, Forth true = -1)", + ), + ( + ": FOO 5 3 < ;", + ["PushI32(0)", "pushi32(0)", "0", "false"], + "Constant fold: PushI32(5), PushI32(3), Lt → PushI32(0) (5 < 3 is false)", + ), + ( + ": FOO DUP DUP DROP DROP ;", + ["Dup", "dup"], + "Peephole: Dup, Dup, Drop, Drop → Dup (first Dup stays, second Dup+Drop cancel, last Drop+implicit cancel... actually: Dup, Dup → keep; Dup, Drop → cancel; left with Dup. Then Drop. Hmm. Let's trace: [Dup, Dup, Drop, Drop] → peephole sees Dup,Drop at positions 1,2 → removes → [Dup, Drop] → peephole sees Dup,Drop → removes → []. Actually empty!", + ), +] + + +def normalize(s: str) -> str: + """Normalize answer for comparison: lowercase, strip whitespace/punctuation.""" + s = s.strip().lower() + # Remove parentheses, brackets, commas for flexible matching + for ch in "()[]": + s = s.replace(ch, "") + # Collapse whitespace + s = " ".join(s.split()) + return s + + +def check_answer(user_input: str, accepted: list[str]) -> bool: + """Check if user's answer matches any accepted answer.""" + norm_input = normalize(user_input) + for ans in accepted: + if normalize(ans) == norm_input: + return True + return False + + +def run_quiz(exercises: list, shuffle: bool = True) -> None: + """Run the interactive quiz.""" + items = list(exercises) + if shuffle: + random.shuffle(items) + + correct = 0 + total = 0 + skipped = 0 + + print("=" * 60) + print(" WAFER IR Flash Quiz") + print(" Predict the optimized IR for each Forth definition.") + print(" Type 'q' to quit, 's' to skip, 'h' for hint.") + print("=" * 60) + print() + + for i, (forth, accepted, explanation) in enumerate(items): + total += 1 + print(f" [{i + 1}/{len(items)}]") + print(f" {forth}") + print() + + while True: + try: + user = input(" Your answer> ").strip() + except (EOFError, KeyboardInterrupt): + print("\n") + show_score(correct, total - 1, skipped) + return + + if user.lower() == "q": + show_score(correct, total - 1, skipped) + return + if user.lower() == "s": + skipped += 1 + print(f" Skipped. Answer: {accepted[0]}") + print(f" {explanation}") + break + if user.lower() == "h": + # Give a hint: first word of explanation + hint_word = explanation.split(":")[0] if ":" in explanation else "Think about the optimizer passes" + print(f" Hint: {hint_word}") + continue + + if check_answer(user, accepted): + correct += 1 + print(f" \033[32m✓ Correct!\033[0m {explanation}") + else: + print(f" \033[31m✗ Not quite.\033[0m Expected: {accepted[0]}") + print(f" {explanation}") + break + + print() + print("-" * 60) + print() + + show_score(correct, total, skipped) + + +def show_score(correct: int, total: int, skipped: int) -> None: + """Display final score.""" + attempted = total - skipped + if attempted == 0: + print(" No questions attempted.") + return + pct = (correct / attempted) * 100 + print(f"\n Score: {correct}/{attempted} ({pct:.0f}%)") + if skipped: + print(f" Skipped: {skipped}") + if pct == 100: + print(" Perfect! You know the optimizer cold.") + elif pct >= 80: + print(" Strong! Review the ones you missed.") + elif pct >= 60: + print(" Getting there. Focus on peephole + fold patterns.") + else: + print(" Study tools/architecture.txt section 7, then retry.") + print() + + +def main() -> None: + """Entry point.""" + if "--all" in sys.argv: + run_quiz(EXERCISES, shuffle=False) + elif "--count" in sys.argv: + print(f"{len(EXERCISES)} exercises available.") + else: + run_quiz(EXERCISES, shuffle=True) + + +if __name__ == "__main__": + main() diff --git a/tools/reading_order.md b/tools/reading_order.md new file mode 100644 index 0000000..fcfb57e --- /dev/null +++ b/tools/reading_order.md @@ -0,0 +1,189 @@ +# WAFER Codebase Reading Order + +Optimal sequence for learning the entire system. Each step builds on the previous. + +--- + +## Phase 1: Mental Model Foundation + +### 1. `crates/core/src/memory.rs` (148 lines) +**Read first.** Defines the physical memory map — every address, every region. You'll reference these constants everywhere else. +- Key insight: stacks grow DOWN, dictionary grows UP +- Memorize: DATA_STACK_TOP=0x1600, DICTIONARY_BASE=0x2D40 +- System variables at offset 0: STATE, BASE, >IN, HERE, LATEST, SOURCE-ID, #TIB, HLD, LEAVE-FLAG +- Notice how regions are laid out to never overlap (verified by compile-time assertions) + +### 2. `crates/core/src/ir.rs` (259 lines) +**The central data structure.** Every Forth word compiles to `Vec`. This is the language the optimizer speaks and the codegen consumes. +- ~70 variants across 10 categories +- Pay attention to control-flow variants: `If`, `DoLoop`, `BeginUntil`, `BeginWhileRepeat`, `BeginDoubleWhileRepeat` — they contain nested `Vec` bodies (tree structure, not flat) +- `Call(WordId)` and `TailCall(WordId)` — how words reference each other +- Float ops are separate from integer ops (separate stack) +- `IrWord` struct: name + body + is_immediate + +### 3. `crates/core/src/error.rs` (84 lines) +**Quick read.** 15 error variants. Note `Throw(i32)` for the Exception word set and `Abort(String)` for ABORT". + +### 4. `crates/core/src/config.rs` (61 lines) +**Quick read.** 7 optimization flags in two tiers: OptConfig (IR-level) and CodegenOpts (codegen-level). Default = all enabled. + +--- + +## Phase 2: Data Structures + +### 5. `crates/core/src/dictionary.rs` (906 lines) +**How words live in memory.** The dictionary is a linked list stored in a `Vec` that simulates WASM linear memory. +- Entry format: link(4) + flags(1) + name(N) + padding + code_field(4) +- Flags byte: IMMEDIATE=0x80, HIDDEN=0x40, LENGTH_MASK=0x1F +- `create()` writes the entry, starts HIDDEN; `reveal()` removes HIDDEN flag +- `find()`: fast path via HashMap index, fallback via linked-list walk +- Wordlist support: `current_wid`, `search_order`, `find_in_wid()` +- `DictionaryState` for MARKER save/restore +- Read every test — they document exact behavior + +--- + +## Phase 3: The Pipeline + +### 6. `crates/core/src/optimizer.rs` (1013 lines) +**IR transformations.** Read the `optimize()` function first to see the pass ordering, then each pass. +- `peephole()`: pattern-match adjacent ops. ~15 patterns. Runs to fixpoint. Study each match arm. +- `constant_fold()`: evaluate PushI32+PushI32+BinaryOp at compile time. Also unary and float. +- `strength_reduce()`: multiply by power-of-2 → shift. 0 compare → ZeroEq/ZeroLt. +- `dce()`: eliminate dead branches (constant condition), truncate after Exit. +- `inline()`: replace Call(id) with body if ≤8 ops, non-recursive, no Exit, no ForthLocals. `detailcall()` converts TailCall back to Call. +- `tail_call_detect()`: last Call → TailCall if return stack balanced. Recurses into If branches. +- Key: `apply_to_bodies()` — every pass recurses into control-flow nested bodies. + +### 7. `crates/core/src/codegen.rs` (4205 lines) — **The Big One** +**IR → WASM translation.** Read in order: +1. **Constants** (lines 1-80): import indices, type indices, DSP/RSP/FSP globals, memory alignment +2. **Helper functions** (lines 80-210): `dsp_dec/inc`, `push_via_local`, `pop`, `peek`, `dsp_writeback/reload`, `rpush_via_local`, `rpop` +3. **Float helpers** (lines 225-330): `fsp_dec/inc`, `fpush_via_local`, `fpop`, `fpeek`, `emit_float_binary/unary/cmp` +4. **`emit_op()`** (line 344+): the giant match — each IrOp variant → WASM instructions. This is the heart. +5. **`compile_word()`**: builds the WASM module structure (imports, types, functions, element section) +6. **`compile_consolidated_module()`**: multi-function module for CONSOLIDATE/export +7. **Stack-to-local promotion**: analysis pass that replaces memory stack operations with WASM locals + +Key patterns to understand: +- DSP cached in local 0: read from global at function entry, write back before calls and at exit +- Scratch locals at SCRATCH_BASE(1): used as temporaries for stack manipulation +- `EmitCtx`: carries f64 locals, Forth local base, loop local base, self_word_id for recursion +- DO/LOOP: index+limit in WASM locals when possible (fast path), fallback to return stack + +--- + +## Phase 4: The Runtime Abstraction + +### 8. `crates/core/src/runtime.rs` (152 lines) +**NEW: Read this before outer.rs.** Defines two traits: +- `Runtime` — abstraction over WASM execution backend (memory, globals, table, module instantiation, host function registration) +- `HostAccess` — memory/global ops available to host function callbacks +- `HostFn = Box Result<()>>` — runtime-agnostic host function type +- Key insight: ForthVM is now `ForthVM`, completely decoupled from wasmtime + +### 8b. `crates/core/src/runtime_native.rs` (328 lines) +**NativeRuntime**: wasmtime implementation of Runtime trait. +- `CallerHostAccess` wraps wasmtime `Caller` to implement `HostAccess` +- `NativeRuntime` owns Engine, Store, Memory, Table, Globals +- `register_host_func`: creates a wasmtime `Func` that bridges `HostFn` → wasmtime callback +- Study how `instantiate_and_install` provides the 6 imports + +### 9. `crates/core/src/outer.rs` — ForthVM struct (lines 1-240) +**Read the struct definition carefully.** ~35 fields. Group them mentally: +- Runtime: `rt: R` (generic over Runtime trait — no more direct wasmtime fields) +- Compilation state: state, compiling_name, compiling_ir, control_stack, compiling_word_id, compiling_locals +- Output: output (Arc>) +- Dictionary bridge: dictionary, user_here, here_cell, base_cell +- Word metadata: ir_bodies, host_word_names, word_pfa_map, does_definitions +- Shared state for host functions: pending_define, pending_actions, pending_does_patch, throw_code, word_lookup +- Configuration: config, batch_mode, deferred_ir +- Export support: toplevel_ir, recording_toplevel +- Advanced: marker_states, conditional_skip_depth, next_block_label, substitutions, search_order, next_wid + +### 10. `crates/core/src/outer.rs` — new() and primitive registration +**How the VM boots.** Read: +- `new_with_config()`: creates `R::new()` runtime, then calls `register_primitives()` and loads boot.fth +- `register_primitive()`: creates dictionary entry → optimizes IR → compiles to WASM → `rt.instantiate_and_install()` +- `register_host_primitive()`: creates dictionary entry → `rt.register_host_func()` with HostFn closure +- `register_primitives()`: ~130 words registered in batch_mode, then `compile_batch()` +- Each host function: study 5-10 representative ones to understand the pattern + +### 11. `crates/core/src/outer.rs` — Outer interpreter loop +**The main loop.** Read: +- `evaluate()`: sets up input buffer, calls `interpret_token()` in a loop +- `interpret_token()`: conditional compilation, `:` handling, `]` handling, dispatch to compile/interpret mode +- `interpret_token_immediate()`: string literals, dictionary lookup, execute found word, parse number +- `compile_token()`: POSTPONE, string literals, control-flow words (IF/ELSE/THEN/DO/LOOP/BEGIN/WHILE/REPEAT/AGAIN/UNTIL/CASE/OF/ENDOF/ENDCASE), dictionary lookup, compile Call(id), parse number → PushI32 +- `finish_colon_def()`: optimize → codegen → install + +### 12. `crates/core/src/outer.rs` — Control flow compilation +**Most complex part.** 13 `ControlEntry` variants. Understand: +- `ControlEntry::If { then_body }` → pushed when IF seen, then_body accumulates until ELSE or THEN +- `ControlEntry::Do { body }` → pushed by DO, body accumulates until LOOP/+LOOP +- `ControlEntry::Begin { body }` → pushed by BEGIN, resolved by UNTIL/AGAIN/WHILE +- `ControlEntry::BeginWhile { test, body }` → WHILE splits Begin into test + body +- `ControlEntry::Case/Of` → CASE/OF/ENDOF/ENDCASE pattern +- `ControlEntry::QDo` → ?DO (conditional entry) +- `ControlEntry::Ahead` → AHEAD (unconditional forward branch) +- CS-PICK and CS-ROLL: advanced control-flow manipulation for tools word set + +--- + +## Phase 5: Self-Hosting + +### 13. `crates/core/boot.fth` (307 lines) +**Forth replaces Rust.** 7 phases of definitions that replace host functions with compiled Forth. +- Phase 1: Stack/memory (DEPTH, PICK, 2OVER, FILL, MOVE, /STRING, -TRAILING) +- Phase 2: Double-cell arithmetic (D+, DNEGATE, D-, DABS, D0=, D0<, D=, D<, DU<) +- Phase 3: Mixed arithmetic (SM/REM, FM/MOD, */, */MOD) — built on M* and UM/MOD host primitives +- Phase 4: HERE, ALLOT, comma, C-comma, ALIGN — magic numbers for sysvar offsets +- Phase 5: I/O and pictured numeric output (TYPE, SPACES, <# HOLD # #S #> . U. .R U.R D. D.R) +- Phase 6: DEFER support (DEFER!, DEFER@) +- Phase 7: String operations, SOURCE, FALIGNED, etc. +- Key insight: why Forth not Rust? Self-hosting goal + compiled Forth with direct calls beats host function dispatch + +--- + +## Phase 6: Production Features + +### 14. `crates/core/src/consolidate.rs` (169 lines) +**Quick read.** Mostly tests. Real logic is in `codegen::compile_consolidated_module()` and `outer::ForthVM::consolidate()`. Understand the concept: merge all JIT modules into one, replacing call_indirect with direct call. + +### 15. `crates/core/src/export.rs` (409 lines) +**wafer build pipeline.** Entry point resolution (--entry > MAIN > top-level), IR collection, memory snapshot, metadata embedding in custom section. + +### 16. `crates/core/src/runner.rs` (402 lines) +**Standalone execution.** Creates the 6 imports from scratch, registers host function stubs for known words (., TYPE, SPACES, .S, M*, UM*, UM/MOD, DEPTH). Shows the minimal set needed to run exported modules. + +### 17. `crates/cli/src/main.rs` (354 lines) +**CLI ties it together.** Three modes: REPL (rustyline), file evaluation, subcommands (build, run). Native executable trick: append AOT payload + "WAFEREXE" trailer to binary. + +### 18. `crates/web/src/lib.rs` (56 lines) +**Browser entry point.** `WaferRepl` struct with `#[wasm_bindgen]`: +- `new()` → `ForthVM::::new()` +- `evaluate(input)` → returns output string +- `data_stack()`, `is_compiling()`, `reset()` + +### 19. `crates/web/src/runtime_web.rs` (542 lines) +**WebRuntime**: browser implementation of Runtime trait. +- Uses `js_sys::WebAssembly` for module instantiation +- `WebHostAccess`: implements HostAccess via `js_sys` typed arrays +- Memory access through `Int32Array`/`Uint8Array` views on `WebAssembly.Memory.buffer` +- Closures kept alive via `_closures: Vec` to prevent GC + +### 20. `crates/web/www/` (727 lines) +**Frontend**: app.js (terminal emulation, stack display), index.html, style.css. + +--- + +## Phase 7: Testing + +### 21. Unit tests (embedded in each source file) +Re-read each file's `#[cfg(test)] mod tests`. They document edge cases and expected behavior. + +### 22. `crates/core/tests/compliance.rs` +Forth 2012 compliance infrastructure: boot_with_prerequisites, run_suite, 11 word set tests. + +### 23. `crates/core/tests/comparison.rs` +Cross-engine benchmarks vs gforth. Performance validation. diff --git a/tools/trace_exercises.md b/tools/trace_exercises.md new file mode 100644 index 0000000..4f21e04 --- /dev/null +++ b/tools/trace_exercises.md @@ -0,0 +1,464 @@ +# WAFER Trace-the-Compilation Exercises + +For each exercise, manually trace the Forth code through the full pipeline: +1. **Outer interpreter** — tokenization, dictionary lookup, compile/interpret dispatch +2. **IR generation** — what Vec is produced +3. **Optimization** — which passes fire, what changes +4. **Codegen** — WASM instructions emitted (conceptual) +5. **Runtime** — how it executes + +Answers are below each exercise (scroll down or cover with paper). + +--- + +## Exercise 1: Simple Arithmetic +```forth +: SQUARE DUP * ; +``` + +
+Answer + +1. `:` → enter compile mode, next token "SQUARE" = word name, dictionary.create("SQUARE") +2. `DUP` → find in dictionary → IR primitive (WordId N) → append `Call(dup_id)` +3. `*` → find → IR primitive → append `Call(mul_id)` +4. `;` → raw IR: `[Call(dup_id), Call(mul_id)]` +5. **Optimize:** + - Inline: DUP body=[Dup] (1 op ≤ 8), * body=[Mul] (1 op ≤ 8) → `[Dup, Mul]` + - Peephole: no patterns match Dup,Mul + - Constant fold: nothing to fold + - Tail call: Mul is not a Call → skip + - **Final IR: `[Dup, Mul]`** +6. **Codegen:** + - Dup: `local.get $dsp; i32.load; local.set $tmp; dsp_dec; local.get $dsp; local.get $tmp; i32.store` + - Mul: `pop; pop; i32.mul; push_via_local` +7. **Runtime:** WASM module instantiated, function registered at table[word_id] + +
+ +--- + +## Exercise 2: Constant Folding +```forth +: TEN 5 5 + ; +``` + +
+Answer + +1. `:` → compile mode, name="TEN" +2. `5` → not in dictionary → parse as number → append `PushI32(5)` +3. `5` → append `PushI32(5)` +4. `+` → find → IR primitive → append `Call(add_id)` +5. `;` → raw IR: `[PushI32(5), PushI32(5), Call(add_id)]` +6. **Optimize:** + - Inline: + body=[Add] → `[PushI32(5), PushI32(5), Add]` + - Constant fold: PushI32(5), PushI32(5), Add → `PushI32(10)` + - **Final IR: `[PushI32(10)]`** +7. **Codegen:** Just `push_const(f, 10)` → `dsp_dec; local.get $dsp; i32.const 10; i32.store` + +
+ +--- + +## Exercise 3: Peephole Elimination +```forth +: NOOP DUP DROP ; +``` + +
+Answer + +1. Raw IR after inlining: `[Dup, Drop]` +2. **Optimize:** + - Peephole: Dup, Drop → removed (both eliminated) + - **Final IR: `[]` (empty)** +3. **Codegen:** Empty function body — just DSP writeback at entry/exit + +
+ +--- + +## Exercise 4: Strength Reduction +```forth +: DOUBLE 8 * ; +``` + +
+Answer + +1. Raw IR after inlining: `[PushI32(8), Mul]` +2. **Optimize:** + - Strength reduce: PushI32(8) is 2^3, so → `[PushI32(3), Lshift]` + - 8 * x becomes x << 3 + - **Final IR: `[PushI32(3), Lshift]`** +3. **Codegen:** push_const(3), then pop two, i32.shl, push result + +
+ +--- + +## Exercise 5: Tail Call Detection +```forth +: FOO 1 + BAR ; +``` +(Assume BAR is a previously defined word) + +
+Answer + +1. Raw IR: `[PushI32(1), Call(add_id), Call(bar_id)]` +2. **Optimize:** + - Inline + (1 op): `[PushI32(1), Add, Call(bar_id)]` + - Tail call: last op is Call(bar_id), return stack balanced (no >R or R>) → `TailCall(bar_id)` + - **Final IR: `[PushI32(1), Add, TailCall(bar_id)]`** +3. **Codegen:** TailCall emits `dsp_writeback; call_indirect bar_id; return` + +
+ +--- + +## Exercise 6: Control Flow — IF/THEN +```forth +: ABS DUP 0< IF NEGATE THEN ; +``` + +
+Answer + +1. `DUP` → Call(dup_id), `0<` → Call(zerolt_id) +2. `IF` → push ControlEntry::If { then_body: [] }, start collecting +3. `NEGATE` → Call(negate_id) appended to then_body +4. `THEN` → pop ControlEntry::If, emit `If { then_body: [Call(negate_id)], else_body: None }` +5. Raw IR: `[Call(dup_id), Call(zerolt_id), If { then: [Call(negate_id)], else: None }]` +6. **Optimize:** + - Inline all (each is 1 op): `[Dup, ZeroLt, If { then: [Negate], else: None }]` + - Note: optimizer recurses into If bodies via apply_to_bodies + - **Final IR: `[Dup, ZeroLt, If { then: [Negate], else: None }]`** +7. **Codegen:** pop flag → `if (block) ... end` WASM structure + +
+ +--- + +## Exercise 7: DO LOOP +```forth +: STARS 0 DO 42 EMIT LOOP ; +``` + +
+Answer + +1. `0` → PushI32(0) +2. `DO` → push ControlEntry::Do { body: [] } +3. `42` → PushI32(42) into body +4. `EMIT` → Call(emit_id) into body +5. `LOOP` → pop Do, emit `DoLoop { body: [PushI32(42), Call(emit_id)], is_plus_loop: false }` +6. Note: the 0 and the limit (already on stack from caller) are consumed by DoLoop +7. **Optimize:** + - Inline EMIT (1 op): `DoLoop { body: [PushI32(42), Emit], is_plus_loop: false }` + - **Final IR:** `[PushI32(0), DoLoop { body: [PushI32(42), Emit], is_plus_loop: false }]` +8. **Codegen:** Loop index+limit in WASM locals. WASM `loop { body; index++; br_if index + +--- + +## Exercise 8: BEGIN UNTIL +```forth +: COUNTDOWN BEGIN DUP . 1 - DUP 0= UNTIL DROP ; +``` + +
+Answer + +1. `BEGIN` → push ControlEntry::Begin { body: [] } +2. `DUP .` → Call(dup_id), Call(dot_id) into body +3. `1 -` → PushI32(1), Call(sub_id) into body +4. `DUP 0=` → Call(dup_id), Call(zeroeq_id) into body +5. `UNTIL` → pop Begin, emit `BeginUntil { body: [Call(dup), Call(dot), PushI32(1), Call(sub), Call(dup), Call(zeroeq)] }` +6. **Optimize:** Inline small primitives. `1 -` stays as `PushI32(1), Sub` (no further fold since operand unknown). `.` is a host function → NOT inlined. +7. `DROP` after loop. + +
+ +--- + +## Exercise 9: Dead Code Elimination +```forth +: ALWAYS-TRUE TRUE IF 42 ELSE 99 THEN ; +``` + +
+Answer + +1. Raw IR after inlining TRUE (body=[PushI32(-1)]): + `[PushI32(-1), If { then: [PushI32(42)], else: Some([PushI32(99)]) }]` +2. **DCE:** PushI32(-1) is nonzero → emit then_body only + → `[PushI32(42)]` +3. Entire IF/ELSE/THEN eliminated. Just pushes 42. + +
+ +--- + +## Exercise 10: Swap Peephole Patterns +```forth +: TEST SWAP SWAP DROP DROP ; +``` + +
+Answer + +1. After inlining: `[Swap, Swap, Drop, Drop]` +2. **Peephole pass 1:** + - Swap, Swap → removed → `[Drop, Drop]` + - Drop, Drop → TwoDrop → `[TwoDrop]` +3. **Final IR: `[TwoDrop]`** + +
+ +--- + +## Exercise 11: Nested Control Flow +```forth +: CLASSIFY DUP 0< IF DROP -1 ELSE 0> IF 1 ELSE 0 THEN THEN ; +``` + +
+Answer + +1. IR structure (after inlining): +``` +[Dup, ZeroLt, If { + then: [Drop, PushI32(-1)], + else: Some([Gt(implicit 0>), If { + then: [PushI32(1)], + else: Some([PushI32(0)]) + }]) +}] +``` +2. Optimizer recurses into both If bodies. No constant conditions → no DCE. +3. Codegen: nested WASM `if/else/end` blocks. + +
+ +--- + +## Exercise 12: DOES> Defining Word +```forth +: CONSTANT CREATE , DOES> @ ; +5 CONSTANT FIVE +FIVE . +``` + +
+Answer + +1. `: CONSTANT` enters compile mode +2. `CREATE` — flagged as saw_create_in_def=true +3. `,` — compiled normally +4. `DOES>` — splits definition: + - create_ir = everything before DOES> (the `,` call) + - does_action = everything after DOES> (the `@` call) → compiled as separate word + - Stores DoesDefinition { create_ir, does_action_id, has_create: true } +5. `5 CONSTANT FIVE`: + - CONSTANT executes its defining behavior + - CREATE makes dictionary entry "FIVE" + - `,` stores 5 at FIVE's parameter field + - DOES> patches FIVE to execute the does_action (which does `@`) +6. `FIVE .`: + - FIVE executes: pushes its PFA, then calls does_action (`@`) + - `@` fetches the 5 stored there + - `.` prints "5 " + +
+ +--- + +## Exercise 13: Consolidation +```forth +: A 1 ; +: B 2 ; +: C A B + ; +CONSOLIDATE +``` + +
+Answer + +1. Before CONSOLIDATE: A, B, C are separate WASM modules. C calls A and B via `call_indirect` through the function table. +2. CONSOLIDATE: + - Collects all IR bodies: A=[PushI32(1)], B=[PushI32(2)], C=[Call(a_id), Call(b_id), Add(inlined)] + - Builds local_fn_map: A→1, B→2, C→3 (within consolidated module) + - `compile_consolidated_module()`: all three become functions in one WASM module + - C's Call(a_id) → direct `call 1` (not call_indirect) + - Replaces all table entries with new functions +3. Result: C calling A and B is now a direct WASM `call` — much faster than table dispatch. + +
+ +--- + +## Exercise 14: Host Function Execution +```forth +5 3 M* +``` + +
+Answer + +1. `5` → push to data stack (dsp -= 4, mem[dsp] = 5) +2. `3` → push to data stack (dsp -= 4, mem[dsp] = 3) +3. `M*` → host function (Rust closure): + - Read sp = dsp global value + - Read n2 = mem[sp] = 3 (as i64) + - Read n1 = mem[sp+4] = 5 (as i64) + - result = 5i64 * 3i64 = 15i64 + - lo = 15 as i32 = 15 + - hi = (15 >> 32) as i32 = 0 + - Write mem[sp+4] = 15 (lo), mem[sp] = 0 (hi) + - Stack unchanged (still 2 cells, now containing double-cell 15) +4. Note: M* is a host function because it needs 64-bit multiplication (WASM i32 only) + +
+ +--- + +## Exercise 15: Float Operations +```forth +: HYPOTENUSE FDUP F* FSWAP FDUP F* F+ FSQRT ; +``` + +
+Answer + +1. After inlining: `[FDup, FMul, FSwap, FDup, FMul, FAdd, FSqrt]` +2. **Peephole:** No matching patterns (FDup+FMul not a known pair) +3. **Codegen:** All float ops use the float stack (FSP global): + - FDup: `fpeek(f)` then `fpush_via_local` + - FMul: `emit_float_binary` with `f64.mul` + - FSqrt: `emit_float_unary` with `f64.sqrt` +4. Float stack lives at 0x2540-0x2D40 in linear memory + +
+ +--- + +## Exercise 16: BEGIN WHILE REPEAT +```forth +: COUNTDOWN BEGIN DUP WHILE DUP . 1 - REPEAT DROP ; +``` + +
+Answer + +1. `BEGIN` → ControlEntry::Begin { body: [] } +2. `DUP` → Call(dup_id) into body +3. `WHILE` → pop Begin, create ControlEntry::BeginWhile { test: [Call(dup_id)], body: [] } +4. `DUP . 1 -` → into body +5. `REPEAT` → pop BeginWhile, emit `BeginWhileRepeat { test: [Dup], body: [Dup, Call(dot_id), PushI32(1), Sub] }` +6. Semantics: evaluate test; if false exit loop; execute body; jump to BEGIN + +
+ +--- + +## Exercise 17: Batch Mode Compilation +```forth +( During ForthVM::new() ) +``` + +
+Answer + +1. `register_primitives()` sets `batch_mode = true` +2. Each `register_primitive("DUP", ...)`: + - Creates dictionary entry (dictionary.create + reveal) + - Stores IR body in ir_bodies + - Pushes `(word_id, ir_body)` to `deferred_ir` (no WASM compilation yet) +3. After all ~40 IR primitives registered: + - `compile_batch()` compiles ALL deferred IR into a single WASM module + - One `rt.instantiate_and_install()` call — single module with ~40 functions + - Each function registered in the table +4. Why batch? Amortizes runtime compilation overhead. One module instead of 40. +5. Host functions bypass batch_mode — registered via `rt.register_host_func()` with HostFn closures. + +
+ +--- + +## Exercise 18: wafer build Pipeline +```forth +( file: hello.fth ) +: MAIN ." Hello, World!" CR ; +``` +```bash +wafer build hello.fth -o hello.wasm +``` + +
+Answer + +1. `cmd_build()`: create ForthVM, set recording=true, evaluate source +2. `evaluate()`: compiles MAIN normally (IR → optimize → codegen) +3. `recording_toplevel=true`: but MAIN is a definition, not top-level execution, so toplevel_ir stays empty +4. `export_module()`: + - Collect IR words: MAIN + all boot.fth definitions + - Entry point: no --entry flag, look for MAIN → found! + - Build `local_fn_map`: all words get module-internal indices + - `compile_exportable_module()`: single WASM module with all functions + - Data section: snapshot of linear memory (dictionary, variables, etc.) + - Metadata in "wafer" custom section: version, entry index, host functions, memory size, stack pointers +5. Output: hello.wasm file + +
+ +--- + +## Exercise 19: Stack-to-Local Promotion +```forth +: ADD3 + + ; +``` + +
+Answer + +1. After inlining: `[Add, Add]` +2. **Stack-to-local promotion** (codegen pass, not optimizer): + - Analyzes stack flow: first Add pops 2, pushes 1; second Add pops 2 (including that 1), pushes 1 + - If stack depth is statically known at each point → can use WASM locals instead of memory stack + - Result: operands stay in WASM locals/operand stack, no memory reads/writes + - Much faster: avoids load/store through linear memory +3. Promotion only works for "straight-line" code (no calls that might modify the stack unpredictably) + +
+ +--- + +## Exercise 20: MARKER and State Restore +```forth +MARKER CLEAN +: FOO 1 ; +: BAR 2 ; +CLEAN +FOO \ Error: unknown word +``` + +
+Answer + +1. `MARKER CLEAN`: + - Creates a MarkerState snapshot: dictionary state, user_here, next_table_index, word_pfa_map, ir_bodies, does_definitions, host_word_names, two_value_words, fvalue_words + - Registers CLEAN as a word that, when executed, restores this snapshot +2. `: FOO 1 ; : BAR 2 ;` — normal compilation, adds to dictionary +3. `CLEAN`: + - Executes the marker word + - Restores dictionary to state before FOO/BAR were defined + - Resets user_here, ir_bodies, etc. + - FOO and BAR are gone — dictionary.find("FOO") returns None +4. `FOO` → "unknown word: FOO" + +Key: MARKER doesn't undo WASM table entries (they become unreachable but stay allocated). It restores the dictionary and Rust-side metadata. + +
diff --git a/tools/wafer_anki.apkg b/tools/wafer_anki.apkg new file mode 100644 index 0000000000000000000000000000000000000000..2c5ad3e83a7278e2a050832bbe08a69cea6376fa GIT binary patch literal 250074 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 literal 0 HcmV?d00001