plum

#treesitter#compiler#wasm

git clone https://git.pyrossh.dev/plum

A statically typed, imperative programming language inspired by rust, python


74c9190 — Peter John 2026-10-26T10:52:24+05:30
docs(plans): keep Int/Float as i64/f64; narrow Int to i32 at extern boundaries
docs/plans/2026-10-26-game-engine.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Plum for games: browser-first macroquad host, port of a simplified KaM
2
2
 
3
3
  **Status:** draft. Planned only and not run; the user has more ideas to fold in first.
4
- **Depends on:** `2026-10-26-zig-generics-and-numbers.md`, which supplies the native `i32 i64 f32 f64 bool` types with i32/f32 literal defaults, duck-typed per-type-copy generics, no traits (enums plus duck-typed operator methods), and `Bytes`. That plan runs first.
4
+ **Depends on:** `2026-10-26-zig-generics-and-numbers.md`, which supplies duck-typed per-type-copy generics, no traits (enums plus duck-typed operator methods), native typed arrays, and `Int`/`Float` kept as i64/f64 with `Int` narrowed to Int at `extern fun` boundaries (no JS BigInt in the browser bridge). That plan runs first.
5
5
  **Goal:** Port `../kam_macroquad`, a 98k-line Rust/macroquad remake of Knights and Merchants that already runs in the browser via wasm, to Plum as a **simplified** game. There is no mission scripting (rhai) and the rules are simpler, but animations and sounds stay identical. It runs in the browser first.
6
6
  **Architecture:** the Rust/macroquad host keeps asset decoding, audio and the window, compiled to wasm32 for the web or native. The Plum wasm-gc module owns game state, rules, pathfinding, AI, animation timing and per-frame draw calls. On the web the two modules talk through JS (miniquad plugin), and natively through wasmtime.
7
7
  **Detail:** tests
@@ -19,31 +19,31 @@ Survey of `../kam_macroquad`:
19
19
  - ~1.9k `&mut self` methods, 967 tests, ~350 macroquad calls.
20
20
  - Collections: `Vec` ×997, `Option` ×814, `HashMap`/`Set`/`BTree`/`VecDeque`/`BinaryHeap` ×145.
21
21
  - Closures and iterator chains ×~3.7k each.
22
- - Numbers: `i32` ×5.2k, `f32` ×1.8k, `u8`/`u16`/`u32`/`usize` ×5.7k (mostly binary asset parsing).
22
+ - Numbers: `Int` ×5.2k, `Float` ×1.8k, `u8`/`u16`/`u32`/`usize` ×5.7k (mostly binary asset parsing).
23
23
  - Traits are small: 7 traits, 63 impls, 40 `dyn`.
24
24
  - The asset side uses flate2, postcard, RON and serde.
25
25
 
26
26
  1. **Split of responsibilities.** Ruling: the host keeps asset decoding (`.rx` sprites/atlases, `kamdat`, flate2, map files), the serde/postcard/RON formats and audio, because that removes almost all the unsigned/binary-parsing and serialization code from the port. The cost is that the host stays Rust and is game-specific.
27
- - The host exposes **handles**: a sprite is `(atlas: i32, rect index: i32)`, and sounds and maps are `i32` ids. Animations stay identical because Plum gets the same frame tables, read through host imports.
27
+ - The host exposes **handles**: a sprite is `(atlas: Int, rect index: Int)`, and sounds and maps are `Int` ids. Animations stay identical because Plum gets the same frame tables, read through host imports.
28
28
  2. **Browser host first.**
29
29
  - `plum-game` is a Rust crate on macroquad. For the web it compiles to wasm32 with macroquad's JS loader.
30
- - A `plum_bridge.js` miniquad plugin instantiates Plum's wasm-gc module, wires Plum's `extern fun` imports to functions exported from the macroquad wasm, and has macroquad's loop call Plum's `init`, `update(g, dt: f32)` and `draw(g)`.
30
+ - A `plum_bridge.js` miniquad plugin instantiates Plum's wasm-gc module, wires Plum's `extern fun` imports to functions exported from the macroquad wasm, and has macroquad's loop call Plum's `init`, `update(g, dt: Float)` and `draw(g)`.
31
31
  - The native build runs the same Plum module under wasmtime (GC, multi-value and SIMD on), with the same extern API.
32
32
  - Ruling: game state lives in the Plum `Game` ref, held by the host and passed to every call, so no mutable globals are needed.
33
33
  3. **Extern API (`plum-std/Game.plum`).**
34
- - Window: `screenSize() -> Vec2` and `frameTime() -> f32`.
35
- - Drawing: `clear(c)`, `drawSprite(atlas, rect, x, y: f32, flipX: bool, tint: Color)`, `drawRect`, `drawLine`, `drawText(s: Str, x, y, size: f32, c)`, `setCamera2D(target: Vec2, zoom: f32)` and `resetCamera()`.
36
- - Input: `isKeyDown(k: Key)`, `isKeyPressed`, `mousePos() -> Vec2`, `isMouseDown(b: MouseButton)` and `mouseWheel() -> f32`.
37
- - Audio: `playSound(id: i32, x, y: f32)` (positional, as in KaM), `playMusic(id)` and `stopMusic()`.
38
- - Assets, as host lookups by name: `atlasId`, `animFrames(unitKind, action, dir) -> Bytes` (packed frame indices) and `animStep`, `loadMap(name) -> MapData`, which copies terrain into Plum `Array[i32]`s once.
34
+ - Window: `screenSize() -> Vec2` and `frameTime() -> Float`.
35
+ - Drawing: `clear(c)`, `drawSprite(atlas, rect, x, y: Float, flipX: Bool, tint: Color)`, `drawRect`, `drawLine`, `drawText(s: Str, x, y, size: Float, c)`, `setCamera2D(target: Vec2, zoom: Float)` and `resetCamera()`.
36
+ - Input: `isKeyDown(k: Key)`, `isKeyPressed`, `mousePos() -> Vec2`, `isMouseDown(b: MouseButton)` and `mouseWheel() -> Float`.
37
+ - Audio: `playSound(id: Int, x, y: Float)` (positional, as in KaM), `playMusic(id)` and `stopMusic()`.
38
+ - Assets, as host lookups by name: `atlasId`, `animFrames(unitKind, action, dir) -> List[Int]` (frame indices, copied once at load) and `animStep`, `loadMap(name) -> MapData`, which copies terrain into Plum `Array[Int]`s once.
39
39
  - `Color` and `Vec2` are structs flattened across the boundary. `Key` and `MouseButton` are enums matching macroquad's codes.
40
40
  4. **Language additions this port needs** (beyond the Zig-generics plan and items 5–7 below):
41
- - **Unboxed typed arrays.** With copies per type, `Array[i32]` and `Array[f32]` lower to native `array (mut i32)` and `array (mut f32)`. These are for tile grids, fog of war, passability and path costs. Add `Array.filled(n, v)` and a 2D index helper.
41
+ - **Unboxed typed arrays.** With copies per type, `Array[Int]` and `Array[Float]` lower to native `array (mut i64)` and `array (mut f64)`. These are for tile grids, fog of war, passability and path costs. Add `Array.filled(n, v)` and a 2D index helper.
42
42
  - **Collections in std:** `Set[T]`, `Deque[T]` (ring buffer) and `Heap[T]` (binary min-heap with a key fn, for A*), all Plum code over `Array`.
43
43
  - **Constants:** large top-level constant tables must parse and be usable in expressions. A probe file with `LIMIT = 10` followed by a `type` failed to parse (cause not yet confirmed), and the notes record a SCREAMING_CASE constant gotcha. The fix needs tests with 1k-entry tables.
44
- - **Seeded RNG** (`Random(seed: i64)` with xorshift/PCG) for deterministic AI and simulation.
44
+ - **Seeded RNG** (`Random(seed: Int)` with xorshift/PCG) for deterministic AI and simulation.
45
45
  - Lazy iteration is deferred. Hot loops use `for … range`, and it's revisited only if profiling shows `map`/`filter` allocation in `update`.
46
- 5. **Value structs** (`struct Vec2(x: f32, y: f32)`, `Color`, `TilePos(x: i32, y: i32)`): flattened locals and params, multi-value returns, copy semantics and in-place field assignment. They are boxed only inside `List`/`Map`/`Try`, and by then they belong to a generic copy, so a `List[Vec2]` copy stores them as GC structs.
46
+ 5. **Value structs** (`struct Vec2(x: Float, y: Float)`, `Color`, `TilePos(x: Int, y: Int)`): flattened locals and params, multi-value returns, copy semantics and in-place field assignment. They are boxed only inside `List`/`Map`/`Try`, and by then they belong to a generic copy, so a `List[Vec2]` copy stores them as GC structs.
47
47
  6. **Duck-typed operators** on structs (`add`/`sub`/`mul`/`neg`/`eq`): `pos + dir * speed`.
48
48
  7. **SIMD (late):** `f32x4` (wasm v128) and `Mat4`/`Vec4` in `plum-std/Math.plum`. KaM is 2D, so this is low priority for the port and kept for future 3D work.
49
49
  8. **Port scope (simplified KaM):**
@@ -71,7 +71,7 @@ Success criteria:
71
71
  - Both suites (`cargo test --workspace`, `plum test plum-std plum-examples`) are green after every task.
72
72
  - The `plum-game` crate builds for `wasm32-unknown-unknown` (web) and native. CI runs the native headless tests.
73
73
  - The asset files are read from `../kam_macroquad/assets` through the host. They are never copied into this repo, because of licensing.
74
- - Every Plum API uses only `i32 i64 f32 f64 bool` scalars. There are no unsigned types.
74
+ - Plum code uses `Int` (i64) and `Float` (f64). The extern API declares `Int`/`Float`, the compiler narrows `Int` to Int at the boundary, and the host converts `Float` to macroquad's Float.
75
75
 
76
76
  ## Tasks
77
77
 
@@ -103,13 +103,13 @@ flowchart LR
103
103
  ```
104
104
 
105
105
  ### L1 typed arrays, collections, constants, RNG
106
- - Native `Array[i32]`/`Array[f32]` (wasm-inspected), `Array.filled` and the 2D helper.
106
+ - Native `Array[Int]`/`Array[Float]` (wasm-inspected), `Array.filled` and the 2D helper.
107
107
  - `Set`, `Deque` and `Heap` in std.
108
108
  - Fix top-level constant parsing and use.
109
109
  - `Random(seed)`.
110
110
 
111
111
  Tests:
112
- - `array_i32_native` (the wasm declares `array (mut i32)`);
112
+ - `array_i32_native` (the wasm declares `array (mut i64)`);
113
113
  - `heap_pop_order` on 1000 random keys;
114
114
  - `deque_wraparound`;
115
115
  - `set_dedup`;
@@ -145,7 +145,7 @@ Tests: a headless mock recorder for call order and args; `color_flattened`; `mou
145
145
  Check: a demo scene (sprites, sound, input) runs in Chrome and Firefox. Playwright headless-Chrome smoke test: canvas pixels change across frames and there are no console errors.
146
146
 
147
147
  ### P1 map + camera
148
- Load a KaM map via the host, terrain into `Array[i32]`, tile rendering, camera pan/zoom, minimap. Check: renders the same as the Rust version on 2 maps (screenshot diff tolerance).
148
+ Load a KaM map via the host, terrain into `Array[Int]`, tile rendering, camera pan/zoom, minimap. Check: renders the same as the Rust version on 2 maps (screenshot diff tolerance).
149
149
 
150
150
  ### P2 units + animations + sounds
151
151
  Unit structs, directional animation state machine, walk interpolation, selection, positional sounds. Tests: the animation frame sequence for walk dir 3 equals the Rust table, and the sound fires on the step frames.
docs/plans/2026-10-26-zig-generics-and-numbers.md CHANGED
@@ -1,41 +1,39 @@
1
- # Zig-style generics, no traits, sized numbers
1
+ # Zig-style generics, no traits (Int/Float stay 64-bit)
2
2
 
3
3
  **Status:** draft. Planned only and not run; the user has more ideas to fold in first.
4
4
  **Goal:**
5
5
  - Replace traits and erased generics with Zig-style duck-typed generics: no bounds, a body checked per instantiation at the use site, and one compiled copy per type.
6
6
  - Use enums for mixed-type values.
7
- - Replace `Int`/`Float`/`Byte`/`Number` with the native wasm value types `i32 i64 f32 f64` (plus `bool`), with literals defaulting to `i32`/`f32` for games.
7
+ - Keep the numeric model simple: `Int` = i64 and `Float` = f64 are the only numeric types. At `extern fun` boundaries `Int` is narrowed to wasm i32, so the browser host never sees a BigInt.
8
8
  - Move Bool's remaining methods into the compiler.
9
9
 
10
10
  **Architecture:** parser → checker (structural `TApp`/`TParam` kept; a generic body is re-checked per concrete instantiation) → instantiation worklist (on-demand, reachable-only) → codegen (unboxed, no `$Obj`/dyn) → hosts.
11
11
  **Detail:** tests
12
12
  **Relation to other plans:**
13
- - This plan replaces Design item 1 and task T1 of `2026-10-26-game-engine.md` (sized numbers).
13
+ - This plan supplies the generics, operators and numeric model that `2026-10-26-game-engine.md` builds on.
14
- - It changes that plan's T4: operator traits become duck-typed operator methods.
15
14
  - It reverses item 3 and part of item 4 of `2026-10-25-erased-generics.md` (trait types, typeId dispatch, boxing at generic edges).
16
15
 
17
16
  ## Design
18
17
 
19
18
  What the user decided:
20
- - Integer literals default to `i32` and float literals to `f32`. Later: only native wasm types, lowercase, no pretend widths (no u8/u32).
21
19
  - Traits are removed in favour of Zig-style duck typing, with one copy per type ("Zig-faithful").
22
20
  - Mixed types are handled with enums only (the Zig way).
23
21
  - Comptime covers duck-typed type params only, with no compile-time execution.
24
22
  - The `[T]` syntax is kept.
25
23
  - Reference: axelmagn.com "Zig traits": traits are a convention (records of functions passed explicitly), not a language feature.
26
-
27
- 1. **Native wasm value types only.** The user decided: no pretend-width types, lowercase names, and only what wasm has.
28
- - Primitives are exactly the wasm value types `i32 i64 f32 f64`, plus `bool` (wasm i32 0/1).
29
- - `Int`, `Float`, `Byte`, `Number`, `Bool`/`True`/`False` and every capitalized numeric name are removed, with no aliases.
30
- - Rule: **lowercase = native wasm value type, Capitalized = heap/GC type** (`Str`, `List`, `Try`, user types).
31
- - Literals: integers default to `i32` and floats to `f32` unless the expected type says otherwise. A literal that doesn't fit its type is a compile error. `true`/`false` are the bool literals.
32
- - Widths never mix implicitly, so `i32 + i64` is an error. Casts are written `i64(x)`, `f32(x)`, `i32(x)` and so on. Truncation/wrap rules are wasm's own: `wrap`, `extend_s`, `trunc_sat_s` and `demote`/`promote`.
33
- - **No unsigned types.** Unsigned operations are explicit methods that map 1:1 to wasm ops: `a.divU(b)`, `remU`, `ltU`/`gtU`/`leU`/`geU`, `shrU`, `toF32U`/`toF64U`, and `i64.fromU32(x)` (`extend_u`). Ruling: this matches the wasm instruction set exactly, so nothing claims a width it doesn't have. The cost is wordier unsigned code.
34
- - **Bytes:** wasm-gc arrays do store packed 8-bit elements, so `Bytes` replaces `[]Byte`/`ByteSlice`. It is a built-in heap type backed by `array (mut i8)` that really is 1 byte per element. `b.get(i) -> i32` (`array.get_u`), `b.getS(i) -> i32` (`array.get_s`) and `b.set(i, v: i32)` (stores the low 8 bits). `Str`/`Buffer` use it. Ruling: the byte width shows up only where storage is really packed, never as a scalar type.
35
- - Ruling: std uses `i64`/`f64` explicitly where precision matters: `Time` millis, `Json` numbers, `Uuid`, `Random`. i32 millis would wrap after about 24 days.
36
- - `parse`/`toStr`: `toStr` and interpolation of primitives are compiler intrinsics. Parsing is `Str.parseI32/parseI64/parseF32/parseF64/parseBool() -> Try[…, [BadNumStr]|[BadBoolStr]]`. `Number.plum`, `Byte.plum` and `ByteSlice.plum` are deleted.
37
- - Grammar: `i32`, `i64`, `f32`, `f64`, `bool`, `true` and `false` become reserved keywords. A type position accepts `primitive_type`. A cast like `i64(x)` parses as a call whose callee is a primitive type. They can no longer be used as identifiers.
38
- 2. **Bool fully built in.** `bool` is a primitive and `Bool.plum` is deleted. `toStr` is an intrinsic, and `and`/`or` are dropped in favour of `&&`/`||`.
24
+ - Numbers: the user first chose i32/f32 defaults, then only native wasm types. They settled on **keeping `Int`/`Float` as i64/f64**, because Plum does no binary decoding (the Rust host does), only game logic.
25
+
26
+ 1. **Numbers stay `Int` (i64) and `Float` (f64).**
27
+ - No sized or unsigned types are added; `Byte`/`[]Byte` stay as they are for `Str`/`Buffer` internals.
28
+ - Wasm i64/f64 arithmetic runs at full speed on 64-bit hosts. The doubled memory for KaM-scale grids (a 128×128 grid is 128 KB) doesn't matter.
29
+ - The `Number = | Int | Float` enum and its implicit coercion stay as today. Generic numeric code is duck-typed (Design item 3), so a `sum[T]` works for both.
30
+ - **Extern narrowing:** in an `extern fun` signature, `Int` params and results lower to wasm **i32**. The value is wrapped going out and sign-extended coming back, and debug builds trap when an `Int` doesn't fit. `Float` stays f64, which is a plain JS number.
31
+ - Ruling: this avoids the browser bridge creating a JS `BigInt` for every i64 argument on hot paths like hundreds of `drawSprite` calls per frame. The cost is that Ints of 2^31 or more can't cross the host boundary, which is fine for handles, ids, key codes and tile indices.
32
+ - Future: if 3D or SIMD is ever needed, add `f32`/`f32x4` then. Not now.
33
+ 2. **Bool fully built in.**
34
+ - Bool is already a built-in i32 (`b3bb7b5`).
35
+ - `toStr` becomes a compiler intrinsic and `parse` becomes `Str.parseBool() -> Try[Bool, [BadBoolStr]]`.
36
+ - `and`/`or` are dropped in favour of `&&`/`||`, and `Bool.plum` is deleted.
39
37
  3. **Duck-typed generics.**
40
38
  - Generic params carry no bounds (`[T]` only). Bound syntax and the `trait` item are removed from the grammar, and existing `trait`/`implements` source becomes a parse error with a migration hint.
41
39
  - A generic body is type-checked once per concrete instantiation, with `TParam`s substituted, at the point the instantiation is first needed.
@@ -44,99 +42,88 @@ What the user decided:
44
42
  4. **Instantiation (copy per type), on demand.**
45
43
  - The worklist starts at `main`, exported fns and tests. Each call site with concrete type args enqueues `(fn, [types])`.
46
44
  - Each key is checked and compiled once, and memoized.
47
- - Generic types (`List[i32]`, `Try[Str, [A]]`) get one GC struct per distinct instantiation, keyed by the structural type, never by mangled strings.
45
+ - Generic types (`List[Int]`, `Try[Str, [A]]`) get one GC struct per distinct instantiation, keyed by the structural type, never by mangled strings.
48
46
  - Only reachable instantiations are produced, which avoids the old "every method for every specialization" problem.
49
- - Names in wasm are only for debugging (`List[i32].map`).
50
- - Ruling: tag-union payload types now go through this structurally (because unions stop being name strings, generic tag payloads become possible; cost: TTags keys move into the instantiation key).
51
- 5. **Unboxed runtime.** `$Obj`, `typeId` headers, `$Box*` and `$dyn$…` dispatchers are removed. Enum variants keep their exact GC subtypes for `match`. The closure ABI is typed per signature again, with no anyref boxing.
52
- 6. **Mixed types via enums.** `List[Shape]` becomes `enum Shape = | Circle(r: f32) | Rect(w: f32, h: f32)` with methods that `match self`. `plum-examples/traits.plum` becomes `shapes.plum` written this way.
47
+ - Names in wasm are only for debugging (`List[Int].map`).
48
+ - Ruling: tag unions go through this structurally, so generic tag payloads become possible.
49
+ 5. **Unboxed runtime.** `$Obj`, `typeId` headers, `$Box*` and `$dyn$…` dispatchers are removed. Enum variants keep their exact GC subtypes for `match`. The closure ABI is typed per signature again, with no anyref boxing. Typed arrays fall out of this: `Array[Int]` and `Array[Float]` copies lower to native `array (mut i64)` and `array (mut f64)`.
50
+ 6. **Mixed types via enums.** `List[Shape]` becomes `enum Shape = | Circle(r: Float) | Rect(w: Float, h: Float)` with methods that `match self`. `plum-examples/traits.plum` becomes `shapes.plum` written this way.
53
51
  7. **Operators are duck-typed.** `a + b` on non-primitive operands calls `a.add(b)` if that method exists for the instantiated type; likewise `sub`, `mul`, `div`, `neg`, `eq` (for `==`) and `lt` (for `<`). Calls are resolved statically per instantiation.
54
52
  8. **Explicit "trait values"** (the article's pattern) need no feature: a record of fn fields (`type Num[T](add: fn(T, T) -> T, zero: T)`) passed as an argument. When the argument is a top-level constant, the copy-per-type codegen calls its fns directly and they can be inlined. Document the pattern in the README.
55
- 9. **Out of scope:** comptime execution (`comptime` blocks, reflection), separate compilation, and any global cap on instantiation count. A warning is printed above 200 instantiations of one generic.
53
+ 9. **Out of scope:** sized/unsigned numeric types, comptime execution (`comptime` blocks, reflection), separate compilation, and any global cap on instantiation count. A warning is printed above 200 instantiations of one generic.
56
54
 
57
55
  Success criteria:
58
56
  - `plum test plum-std plum-examples` and `cargo test --workspace` are green.
59
57
  - A typical compile is ≤ 60 ms, against today's 20–25 ms erased. That's the agreed cost of copying per type.
60
- - `List[i32].map` in the wasm touches no boxes.
58
+ - `List[Int].map` in the wasm touches no boxes.
61
59
  - A generic called with a type lacking a method fails with the use-site chain message.
60
+ - An `extern fun f(x: Int) -> Int` is imported with wasm type `(i32) -> i32`.
62
61
 
63
62
  ## Global constraints
64
63
 
65
64
  - Both suites are green after every task. Regenerate the grammar only with `--disable-optimizations`.
66
- - Error wording: `in <fn>[<types>] (called at <file>:<line>): <Type> has no method '<m>'`; `cannot mix i32 and i64 without a cast`; `literal 3000000000 does not fit in i32`.
65
+ - Error wording: `in <fn>[<types>] (called at <file>:<line>): <Type> has no method '<m>'`.
67
66
  - No `$` name mangling anywhere. The instantiation key is `(item path, Vec<PlumType>)`.
68
67
 
69
68
  ## Tasks (sequential)
70
69
 
71
70
  ```mermaid
72
71
  flowchart LR
73
- T1[sized numbers + literal typing]
72
+ T1[extern Int narrowing + Bool.plum removal]
74
- T2[Bool/Number std removal]
75
- T3[per-instantiation checking]
73
+ T2[per-instantiation checking]
76
- T4[on-demand instantiation codegen]
74
+ T3[on-demand instantiation codegen]
77
- T5[remove traits, dyn, boxing]
75
+ T4[remove traits, dyn, boxing]
78
- T6[enums for mixed types + operators]
76
+ T5[enums for mixed types + operators]
79
- T7[std/examples migration + docs + bench]
77
+ T6[migration + docs + bench]
80
78
  %% taskgraph
81
79
  T1 --> T2
82
80
  T2 --> T3
83
81
  T3 --> T4
84
82
  T4 --> T5
85
83
  T5 --> T6
86
- T6 --> T7
87
84
  ```
88
85
 
89
- ### T1 native value types + literal typing
90
- Grammar: `i32 i64 f32 f64 bool true false` become reserved keywords and `primitive_type` is added to `type`; regenerate with `--disable-optimizations`. Parser, checker and codegen get the four numeric types, `bool` and the cast calls. Contextual literals default to i32/f32. Add the mixing error and the explicit unsigned methods. Tests:
91
- - `i32_overflow_wraps`: `i32(2147483647) + 1 == -2147483648`.
92
- - `unsigned_div_method`: `i32(-2).divU(2) == 2147483647`.
93
- - `f32_exact_bits` for `0.1`.
94
- - `literal_defaults`: `x := 5` is i32, `y := 1.5` is f32.
95
- - `mix_error`: `i32 + i64` is rejected.
96
- - `literal_overflow_error`: `x: i32 = 3000000000`.
97
- - `keyword_not_identifier`: `i32 := 1` is a parse error.
98
- - `cast_trunc_sat`: `i32(f32(1e10)) == 2147483647`.
99
-
100
- ### T2 Bytes + std migration off Int/Float/Byte/Bool
101
- Add the built-in `Bytes` (packed `array (mut i8)`, `get`/`getS`/`set`/`len`). `Str`/`Buffer` move to it. Delete Bool.plum, Number.plum, Byte.plum and ByteSlice.plum. Add intrinsic `toStr`/interpolation for primitives and `Str.parse*`. Rewrite every `Int`/`Float`/`Byte`/`Bool`/`True`/`False` in plum-std, choosing the width per use (i64/f64 for Time/Json/Uuid/Random). Tests:
102
- - the existing std suites;
103
- - `bytes_one_byte_per_elem`: the wasm declares `array (mut i8)`;
104
- - `bytes_get_u_vs_s`: storing 200 gives `get == 200` and `getS == -56`;
105
- - `time_millis_no_wrap`: `nowMillis() > 2147483647`.
106
-
107
- ### T3 per-instantiation checking
86
+ ### T1 extern Int narrowing + Bool.plum removal
87
+ `Int` in `extern fun` signatures lowers to i32: wrap out, sign-extend in, and a debug-build range trap. Update the hosts (plum-cli, plum-runtime, examples_test linkers) for the narrowed import types. Delete Bool.plum, add an intrinsic `Bool.toStr`, and add `Str.parseBool`. Tests:
88
+ - `extern_int_is_i32`: the import type is `(i32) -> i32`;
89
+ - `extern_int_roundtrip_negative`: -5 out and back is still -5;
90
+ - `extern_int_overflow_traps_debug`;
91
+ - `bool_tostr_intrinsic`: `"{true}"` is `"True"`;
92
+ - `parse_bool`.
93
+
94
+ ### T2 per-instantiation checking
108
95
  Re-check generic bodies per concrete instantiation and produce the use-site chain errors. Bounds are removed from the checker. Tests:
109
96
  - `duck_ok`: `max[Vec2]` works when Vec2 has `lt`.
110
97
  - `duck_missing_method_chain`: the message includes `called at`.
111
98
  - `uninstantiated_generic_not_type_errored`.
112
99
  - `nested_instantiation_chain`: two levels deep.
113
100
 
114
- ### T4 on-demand instantiation codegen
101
+ ### T3 on-demand instantiation codegen
115
- Worklist from roots, one wasm fn/struct per `(item, types)`, structural keys, unboxed values, typed closures. Tests:
102
+ Worklist from roots, one wasm fn/struct per `(item, types)`, structural keys, unboxed values, typed closures, native typed arrays. Tests:
116
- - `one_copy_per_distinct_type`: List[i32] and List[Str] give 2 `map`s, and List[i32] used twice still gives 1.
103
+ - `one_copy_per_distinct_type`: List[Int] and List[Str] give 2 `map`s, and List[Int] used twice still gives 1.
117
104
  - `unreachable_generic_not_emitted`.
118
- - `no_boxes_in_list_i32_map` (wasm inspection).
105
+ - `no_boxes_in_list_int_map` (wasm inspection).
106
+ - `array_int_native`: the wasm declares `array (mut i64)`.
119
- - `generic_tag_payload`: `Try[i32, [Bad(T)]]` works.
107
+ - `generic_tag_payload`: `Try[Int, [Bad(T)]]` works.
120
108
 
121
- ### T5 remove traits, dyn, boxing
109
+ ### T4 remove traits, dyn, boxing
122
110
  Remove the grammar's `trait`/`implements`/bounds, the checker's trait tables and coercion, and codegen's `$Obj`/typeId/`$Box*`/`$dyn` and erased paths. Tests: `trait_syntax_rejected_with_hint`. Workspace and plum suites green. Report the LOC delta.
123
111
 
124
- ### T6 enums for mixed types + duck-typed operators
112
+ ### T5 enums for mixed types + duck-typed operators
125
113
  `traits.plum` becomes `shapes.plum` (enum Shape). Operator methods `add/sub/mul/div/neg/eq/lt`. The README documents the explicit "trait values" pattern (Design item 8). Tests:
126
114
  - `shape_enum_area_sum`.
127
115
  - `vec2_add_operator`.
128
116
  - `operator_missing_method_error`.
129
- - `num_record_passed_explicitly` (the article's pattern, e.g. `sumWith(nums, i32Num)`).
117
+ - `num_record_passed_explicitly` (the article's pattern, e.g. `sumWith(nums, intNum)`).
130
118
 
131
- ### T7 migration, docs, bench
132
- Migrate every example, test and README snippet to sized types and no traits. Update the README sections "How generics compile" (now per-type copies, Zig-style) and "Numbers". Rebuild the scratchpad bench and record compile times and wasm sizes against the erased baseline (typical 20–25 ms, Bool.wasm 19 KB). Update the game-engine plan's T1/T4 to point here.
119
+ ### T6 migration, docs, bench
120
+ Migrate every example, test and README snippet off traits. Update the README sections "How generics compile" (now per-type copies, Zig-style) and "extern functions" (Int narrowed to i32). Rebuild the scratchpad bench and record compile times and wasm sizes against the erased baseline (typical 20–25 ms, Bool.wasm 19 KB).
133
121
 
134
122
  ## Rulings (user said "you decide")
135
123
 
136
124
  - Ruling: the instantiation warning stays at 200 copies of one generic. It is only a warning, never an error.
137
- - Ruling: `"{x}"` on a type with no `toStr` is a duck-typed compile error at the use site. Primitives, `Str`, `bool` and tag unions format through compiler intrinsics.
125
+ - Ruling: `"{x}"` on a type with no `toStr` is a duck-typed compile error at the use site. Primitives, `Str`, `Bool` and tag unions format through compiler intrinsics.
138
- - Ruling: `Str` stays capitalized, because it's a GC heap type, following "lowercase = wasm value type".
139
- - Ruling: comptime performance means copies per type (Design item 4) and unboxed native values everywhere. That's why the generic boxing paths are removed in T5.
126
+ - Ruling: comptime performance means copies per type (Design item 4) and unboxed native values everywhere. That's why the generic boxing paths are removed in T4.
140
127
 
141
128
  ## Open questions
142
129