You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
9
9
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.
11
11
12
12
Classes support single-level inheritance, `super()`, full dunder dispatch, `@property` / `@x.setter`. Functional-first: composition preferred, monomorphic dispatch optimised via instance-dunder IC.
13
13
@@ -23,16 +23,16 @@ Classes support single-level inheritance, `super()`, full dunder dispatch, `@pro
23
23
24
24
## Bytecode shape
25
25
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.
27
27
28
28
```text
29
-
OpCode::LoadConst -> operand = constant index
29
+
OpCode::LoadConst -> operand = constant index
30
30
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
36
36
```
37
37
38
38
## Dispatch shape
@@ -41,7 +41,7 @@ Hot loop reads `cache.fused_ref()[ip]`, a snapshot of the instruction stream wit
41
41
42
42
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.
43
43
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.
45
45
46
46
## Memory model
47
47
@@ -59,9 +59,9 @@ For arith/compare opcodes, the loop checks `cache.get_fast(ip)`: if a `FastOp` i
59
59
60
60
`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.
61
61
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.
63
63
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.
65
65
66
66
`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>)`.
67
67
@@ -72,18 +72,18 @@ Heap is a `Vec<HeapSlot>` arena with a free list (capped 524,288, sorted to pref
72
72
- No dead-store elimination beyond what falls out of constant folding.
73
73
- No IR, one representation between source and dispatch.
74
74
- 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).
76
76
- 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)).
77
77
78
78
## Coroutine and context-manager dispatch
79
79
80
80
`async def` and `yield`-bearing `def` both produce `HeapObj::Coroutine`. `run()` drives the scheduler; the other primitives are top-level builtins ([Async](/language/async)).
81
81
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.
83
83
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.
85
85
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.
0 commit comments