Skip to content

Commit 82982f3

Browse files
docs(design): Allign docs and clean redaction.
1 parent 84534a5 commit 82982f3

1 file changed

Lines changed: 16 additions & 16 deletions

File tree

docs/pages/implementation/design.md

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ description: "Compiler architecture, dispatch model, and runtime layout."
55

66
## Overview
77

8-
Release build, is a 200 KB on `wasm32-unknown-unknown` (`panic=abort`, `opt-level=z`, `lto=true`, `codegen-units=1`). Where the path is: LUT-driven lexer -> single-pass Pratt parser emitting SSA-versioned bytecode directly -> peephole constant-folding optimiser -> token-threaded interpreter with two layers of adaptive specialisation.
8+
The release build is less than 200 KB on `wasm32-unknown-unknown` (`panic=abort`, `opt-level=z`, `lto=true`, `codegen-units=1`). The pipeline: LUT-driven lexer -> single-pass Pratt parser emitting SSA-versioned bytecode directly -> peephole constant-folding optimiser -> token-threaded interpreter with two layers of adaptive specialisation.
99

10-
No AST, no IR, bytecode is the only intermediate representation. Around 13,000 lines of Rust; production deps are `hashbrown` and `itoa` (SHA-256 in-tree). WASM build adds `lol_alloc` for a single-threaded leaking bump allocator.
10+
No AST, no IR: bytecode is the only intermediate representation. Around 13,000 lines of Rust; production deps are `hashbrown` and `itoa` (SHA-256 in-tree). WASM build adds `lol_alloc` for a single-threaded leaking bump allocator.
1111

1212
Classes support single-level inheritance, `super()`, full dunder dispatch, `@property` / `@x.setter`. Functional-first: composition preferred, monomorphic dispatch optimised via instance-dunder IC.
1313

@@ -23,16 +23,16 @@ Classes support single-level inheritance, `super()`, full dunder dispatch, `@pro
2323

2424
## Bytecode shape
2525

26-
Each `Instruction` is 4 bytes: 1-byte `OpCode` (`#[repr(u8)]` planned), 2-byte operand, 1 byte padding. Opcodes span 17 categories, load, store, arith, bitwise, compare, logic, identity, control flow, iter, build, container, comprehension, function, ssa (Phi), yield, side effects, unsupported (raises at runtime). Around 40 specialised `Call*` variants for hot builtins; `LoadAttr + Call(0)` pairs fuse into `CallMethod + CallMethodArgs` after first dispatch.
26+
Each `Instruction` is 4 bytes: 1-byte `OpCode` (`#[repr(u8)]` planned), 2-byte operand, 1 byte padding. Opcodes span 17 categories: load, store, arith, bitwise, compare, logic, identity, control flow, iter, build, container, comprehension, function, ssa (Phi), yield, side effects, unsupported (raises at runtime). Around 40 specialised `Call*` variants for hot builtins; `LoadAttr + Call(0)` pairs fuse into `CallMethod + CallMethodArgs` after first dispatch.
2727

2828
```text
29-
OpCode::LoadConst -> operand = constant index
29+
OpCode::LoadConst -> operand = constant index
3030
OpCode::LoadName -> operand = name slot
31-
OpCode::StoreName -> operand = name slot
32-
OpCode::Add / Sub -> operand = 0 (IC slot derived from ip)
33-
OpCode::Call -> operand = (kw << 8) | pos
34-
OpCode::Phi -> operand = target slot, sources in chunk.phi_sources
35-
OpCode::ForIter -> operand = jump target on iterator exhaustion
31+
OpCode::StoreName -> operand = name slot
32+
OpCode::Add / Sub -> operand = 0 (IC slot derived from ip)
33+
OpCode::Call -> operand = (kw << 8) | pos
34+
OpCode::Phi -> operand = target slot, sources in chunk.phi_sources
35+
OpCode::ForIter -> operand = jump target on iterator exhaustion
3636
```
3737

3838
## Dispatch shape
@@ -41,7 +41,7 @@ Hot loop reads `cache.fused_ref()[ip]`, a snapshot of the instruction stream wit
4141

4242
For arith/compare opcodes, the loop checks `cache.get_fast(ip)`: if a `FastOp` is present, it runs inline without a function call; type-guard miss invalidates the slot and falls back to the generic handler. IC is per-instruction, so monomorphic sites stabilise independently.
4343

44-
`LoadConst` reads a pre-materialised `Vec<Val>` (`OpcodeCache::const_vals`) built on first dispatch. Inline-range ints (47-bit) stored inline; 2⁴⁷,2¹²⁷ allocate a `HeapObj::LongInt` slot. Literals beyond ±2¹²⁷ are rejected at parse time.
44+
`LoadConst` reads a pre-materialised `Vec<Val>` (`OpcodeCache::const_vals`) built on first dispatch. Inline-range ints (47-bit) stored inline; ints from 2⁴⁷ to 2¹²⁷ allocate a `HeapObj::LongInt` slot. Literals beyond ±2¹²⁷ are rejected at parse time.
4545

4646
## Memory model
4747

@@ -59,9 +59,9 @@ For arith/compare opcodes, the loop checks `cache.get_fast(ip)`: if a `FastOp` i
5959

6060
`INT_MAX = 140_737_488_355_327`, `INT_MIN = -140_737_488_355_328`. Inline ints take one ALU op per arithmetic; overflow promotes to `HeapObj::LongInt(i128)` until results fit inline again. LongInts are interned by value so equal values share a heap index (consistent `hash`/`eq`). Hard cap ±2¹²⁷; wider raises `OverflowError`. Arbitrary-precision bigints would need a `Vec<u32>`-limb variant (heap-allocs per op, Knuth D / Karatsuba code) or dropping NaN-boxing, both regress WASM-size and inner-loop goals.
6161

62-
`PartialEq` / `Hash` for `Val` funnel value-equal numerics through `f64` bits so `1 == 1.0` and `hash(1) == hash(1.0)`, dicts/sets see them as one key. `FxBuildHasher` uses a fixed seed for reproducible iteration order across runs.
62+
`PartialEq` / `Hash` for `Val` funnel value-equal numerics through `f64` bits so `1 == 1.0` and `hash(1) == hash(1.0)` dicts/sets see them as one key. `FxBuildHasher` uses a fixed seed for reproducible iteration order across runs.
6363

64-
Heap is a `Vec<HeapSlot>` arena with a free list (capped 524,288, sorted to prefer low indices). Strings, bytes (<=128 B), and LongInts are interned in side hashes so equal values collapse to one slot, short literals short-circuit through identity and dict/set lookups stay consistent across allocations. Live-object cap is `Limits.heap` (default 10M; sandbox 100K). Single-colour mark-and-sweep, no refcount, cycles reclaimed natively.
64+
Heap is a `Vec<HeapSlot>` arena with a free list (capped 524,288, sorted to prefer low indices). Strings, bytes (<=128 B), and LongInts are interned in side hashes, so equal values collapse to one slot, short literals short-circuit through identity, and dict/set lookups stay consistent across allocations. Live-object cap is `Limits.heap` (default 10M; sandbox 100K). Single-colour mark-and-sweep, no refcount, cycles reclaimed natively.
6565

6666
`HeapObj` variants: `Str`, `Bytes`, `List` (`Rc<RefCell<Vec<Val>>>`), `Dict` (insertion-ordered), `Set`, `FrozenSet`, `Tuple`, `Func(fn_idx, defaults, captures)`, `Range`, `Slice`, `Ellipsis` (singleton, distinct from `'...'`), `Type`, `ExcInstance`, `BoundMethod`, `NativeFn`, `Class(name, members)`, `Instance(class, attrs)`, `BoundUserMethod(recv, fn)`, `Coroutine(ip, slots, stack, body, iter_stack, sync_frames)` (shared by generators, `async def`, and the implicit module-body coro; `body` is `BodyRef::Fn(usize)` or `BodyRef::Module`; `sync_frames` stacks suspended sync sub-calls so a plain `def` hitting a yielding builtin can resume mid-body), `Module(spec, attrs)`, `Extern(Arc<dyn Fn>)`.
6767

@@ -72,18 +72,18 @@ Heap is a `Vec<HeapSlot>` arena with a free list (capped 524,288, sorted to pref
7272
- No dead-store elimination beyond what falls out of constant folding.
7373
- No IR, one representation between source and dispatch.
7474
- No JIT (single-tier, pure Rust). Method JITs need per-arch stencils; trace JITs duplicate the execution model and complicate GC.
75-
- No runtime module system, imports resolve at parse time through a host-injected `Resolver`. See [Imports](/reference/imports).
75+
- No runtime module system imports resolve at parse time through a host-injected `Resolver`. See [Imports](/reference/imports).
7676
- No bigints, complex numbers, `bytearray`, `memoryview`, `Decimal`, `Fraction`. No `gen.send` / `throw` / `close`. No `asyncio` module — concurrency primitives are top-level builtins ([Async](/language/async)).
7777

7878
## Coroutine and context-manager dispatch
7979

8080
`async def` and `yield`-bearing `def` both produce `HeapObj::Coroutine`. `run()` drives the scheduler; the other primitives are top-level builtins ([Async](/language/async)).
8181

82-
A plain `def` inside a coroutine calling a yielding builtin gets its state (`ip`, slots, stack/iter deltas) snapshotted as a `SyncFrame` pushed on the enclosing Coroutine's `sync_frames` (innermost-last). `resume_coroutine` walks this stack inside-out before re-entering the outer body, so each helper's return value lands at the original `Call` site, otherwise the outer's `resume_ip` would skip past the unfinished helper.
82+
A plain `def` inside a coroutine calling a yielding builtin gets its state (`ip`, slots, stack/iter deltas) snapshotted as a `SyncFrame` pushed on the enclosing Coroutine's `sync_frames` (innermost-last). `resume_coroutine` walks this stack inside-out before re-entering the outer body, so each helper's return value lands at the original `Call` site; otherwise the outer's `resume_ip` would skip past the unfinished helper.
8383

84-
`vm.run()` wraps the module body as an implicit coroutine with `BodyRef::Module`, top-level statements suspend like `async def` bodies. Single-driver: `top_loop` is the only place that picks coros; `run` / `gather` / `with_timeout` are non-driving, they push targets to the scheduler, park the outer in `CoroState::WaitingForChildren { tasks, kind: WaitKind }`, yield. `WaitKind` picks finalize behavior: `Run(target)` returns its value, `Gather` returns the list of results, `Timeout { deadline_ns, target }` enforces a deadline. `wake_waiting_outers` (gated by `waiting_for_children_count`) drops terminal children, splices the result into the outer's saved stack placeholder, marks the outer `Ready`, or `raise_into_outer` injects the exception.
84+
`vm.run()` wraps the module body as an implicit coroutine with `BodyRef::Module`; top-level statements suspend like `async def` bodies. Single-driver: `top_loop` is the only place that picks coros; `run` / `gather` / `with_timeout` are non-driving: they push targets to the scheduler, park the outer in `CoroState::WaitingForChildren { tasks, kind: WaitKind }`, and yield. `WaitKind` picks finalize behavior: `Run(target)` returns its value, `Gather` returns the list of results, and `Timeout { deadline_ns, target }` enforces a deadline. `wake_waiting_outers` (gated by `waiting_for_children_count`) drops terminal children, splices the result into the outer's saved stack placeholder, marks the outer `Ready`, or `raise_into_outer` injects the exception.
8585

86-
Coroutines carry their own `exception_frames` (7th tuple field). `resume_coroutine` denormalises stored depths (relative to `saved_stack_len` / `saved_iter_len`) on entry, pushes them onto the live `exception_stack`, renormalises on yield-save; `dispatch.rs::exec` honors `pending_exec_exc_base` so handler search includes restored frames. Net: `try`/`except` survives yields, `try: run(coro) except E:` catches a child's raise across multiple `run_resume` cycles. `SyncFrame.exception_delta` does the same for sync-helper try blocks spanning a yield.
86+
Coroutines carry their own `exception_frames` (7th tuple field). `resume_coroutine` denormalises stored depths (relative to `saved_stack_len` / `saved_iter_len`) on entry, pushes them onto the live `exception_stack`, renormalises on yield-save; `dispatch.rs::exec` honors `pending_exec_exc_base` so handler search includes restored frames. Net: `try`/`except` survives yields; `try: run(coro) except E:` catches a child's raise across multiple `run_resume` cycles. `SyncFrame.exception_delta` does the same for sync-helper try blocks spanning a yield.
8787

8888
`with` invokes `__enter__` / `__exit__(exc_type, exc_val, traceback)`; truthy `__exit__` return suppresses. `async with` reuses sync `__enter__` / `__exit__` (no async dunders).
8989

0 commit comments

Comments
 (0)