plum

#treesitter#compiler#wasm

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

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


bbcbddc — Peter John 2026-10-26T10:38:51+05:30
docs(plans): native lowercase wasm types only (i32/i64/f32/f64/bool), Bytes for packed storage
docs/plans/2026-10-26-game-engine.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Plum for games (macroquad host, sized numbers, value structs, SIMD)
2
2
 
3
3
  **Status:** draft. Planned only, not dispatched, because the user has more ideas to fold in first.
4
- **Superseded in part:** Design item 1 with T1 (sized numbers), and T4 (operator traits), now live in `2026-10-26-zig-generics-and-numbers.md`. That plan has the sized types with I32/F32 literal defaults, and no traits: operators are duck-typed methods, and enums replace trait types.
4
+ **Superseded in part:** Design item 1 with T1 (sized numbers), and T4 (operator traits), now live in `2026-10-26-zig-generics-and-numbers.md`. That plan has the native wasm value types i32/i64/f32/f64 with i32/f32 literal defaults (lowercase; no U8/i32 — unsigned via explicit ops, packed bytes via `Bytes`), and no traits: operators are duck-typed methods, and enums replace trait types.
5
5
  **Goal:** Let Plum write game logic and rendering, running on a macroquad-based host. Natively first, then in the browser.
6
6
  **Architecture:** Plum source → checker → erased wasm-gc codegen → `plum-game`, a Rust crate built on macroquad and wasmtime that drives `init`/`update`/`draw` each frame.
7
7
  **Tech stack:** Rust, macroquad, wasmtime (GC, multi-value and SIMD enabled), wasm-encoder.
@@ -15,31 +15,31 @@ What the user decided:
15
15
  - Value types come from a new `struct` syntax.
16
16
  - Vectors and simd128.
17
17
 
18
- 1. **Sized numbers.** `F32`, `I32` and `U32` are new built-in primitives (wasm `f32`/`i32`). `Byte` stays as u8, `Int` as i64 and `Float` as f64.
18
+ 1. **Sized numbers.** `f32`, `i32` and `i32` are new built-in primitives (wasm `f32`/`i32`). `Byte` stays as u8, `Int` as i64 and `Float` as f64.
19
- - Ruling: `Float` is not changed to f32 (because Time millis, Json and parseFloat need f64; cost: game code writes `F32` explicitly).
19
+ - Ruling: `Float` is not changed to f32 (because Time millis, Json and parseFloat need f64; cost: game code writes `f32` explicitly).
20
- - Arithmetic, comparisons and `toStr`/`parse*` exist per type, and explicit casts are `F32(x)`, `I32(x)` and so on.
20
+ - Arithmetic, comparisons and `toStr`/`parse*` exist per type, and explicit casts are `f32(x)`, `i32(x)` and so on.
21
21
  - Each type boxes at generic edges with its own typeId (8, 9 and 10).
22
- 2. **Contextual literal typing.** A numeric literal takes its type from the expected type (annotation, param, field, operand) where that type is a sized type. So `x: F32 = 1.5`, `drawRect(10, 20, 32, 32)` and `v.x * 0.5` need no casts. With no expected type, literals default to `Int`/`Float` as today. A literal that doesn't fit the target (e.g. `I32 = 3_000_000_000`) is a compile error.
23
- 3. **Value structs.** `struct Vec2(x: F32, y: F32)` has the same body syntax as a class, but has value semantics.
22
+ 2. **Contextual literal typing.** A numeric literal takes its type from the expected type (annotation, param, field, operand) where that type is a sized type. So `x: f32 = 1.5`, `drawRect(10, 20, 32, 32)` and `v.x * 0.5` need no casts. With no expected type, literals default to `Int`/`Float` as today. A literal that doesn't fit the target (e.g. `i32 = 3_000_000_000`) is a compile error.
23
+ 3. **Value structs.** `struct Vec2(x: f32, y: f32)` has the same body syntax as a class, but has value semantics.
24
24
  - In locals and params a struct is flattened into one wasm local/param per field, and it is returned with multi-value results.
25
25
  - Assigning copies it. A field can be assigned in place (`v.x += 1.0`), which mutates only that binding.
26
26
  - It is boxed (as a GC struct with a typeId) only when it flows into a `TParam`, trait slot, `List` element or closure capture.
27
27
  - `==` compares fields. Methods take `self` by value. Structs can't be recursive.
28
28
  4. **Operator traits.** `trait Add = add(other: Self) -> Self` (plus `Sub`, `Mul`, `Div`, `Neg` and `Eq`) maps `+ - * / -x ==` on non-primitive operands to those methods. Calls are resolved statically and are never dyn dispatched. `Mul` may also take a scalar RHS (`v * 2.0`) through an overload on the param type.
29
- 5. **SIMD.** `F32x4` is a primitive (wasm `v128`) with lane-wise `+ - * /`, `F32x4.splat(x)`, `.x/.y/.z/.w` lane access, `dot`, `min`, `max` and `shuffle`. `plum-std/Math.plum` builds `Vec4`, `Quat` and `Mat4` (four `F32x4` columns) on top of it. `Vec2`/`Vec3` stay scalar structs, plus `lerp`, `clamp`, `perspective`, `lookAt` and `ortho`.
29
+ 5. **SIMD.** `f32x4` is a primitive (wasm `v128`) with lane-wise `+ - * /`, `f32x4.splat(x)`, `.x/.y/.z/.w` lane access, `dot`, `min`, `max` and `shuffle`. `plum-std/Math.plum` builds `Vec4`, `Quat` and `Mat4` (four `f32x4` columns) on top of it. `Vec2`/`Vec3` stay scalar structs, plus `lerp`, `clamp`, `perspective`, `lookAt` and `ortho`.
30
30
  6. **`plum-game` host (native).** A new crate built on macroquad and wasmtime.
31
- - It loads `game.wasm`, calls the exported `init() -> Game` once, keeps the returned ref (a wasmtime `Rooted`), and every frame calls `update(g, dt: F32)` and `draw(g)`.
31
+ - It loads `game.wasm`, calls the exported `init() -> Game` once, keeps the returned ref (a wasmtime `Rooted`), and every frame calls `update(g, dt: f32)` and `draw(g)`.
32
32
  - Ruling: no global mutable state and no new language feature (because the state lives in the host; cost: the `Game` ref is passed on every call).
33
33
  - The extern API lives in `plum-std/Game.plum`:
34
- - `clear(c: Color)`, `drawRect(x, y, w, h: F32, c: Color)`, `drawCircle`, `drawLine`, `drawText(s: Str, x, y, size: F32, c: Color)`;
34
+ - `clear(c: Color)`, `drawRect(x, y, w, h: f32, c: Color)`, `drawCircle`, `drawLine`, `drawText(s: Str, x, y, size: f32, c: Color)`;
35
- - `loadTexture(path: Str) -> U32` (preloaded by the host before `init`), `drawTexture(t: U32, x, y: F32, c: Color)`;
35
+ - `loadTexture(path: Str) -> i32` (preloaded by the host before `init`), `drawTexture(t: i32, x, y: f32, c: Color)`;
36
36
  - `isKeyDown(k: Key) -> Bool`, `isKeyPressed`, `mousePos() -> Vec2`, `isMouseDown`, `screenSize() -> Vec2`;
37
- - `playSound(s: U32)`, `setCamera2D(target: Vec2, zoom: F32)`.
37
+ - `playSound(s: i32)`, `setCamera2D(target: Vec2, zoom: f32)`.
38
38
  - Structs cross the boundary flattened, so `Color` is 4 f32 params. `Key` is a discriminant enum whose values match macroquad's `KeyCode`.
39
39
  - `plum game <file>` is a CLI subcommand that compiles the file and runs it in the host.
40
40
  7. **Custom rendering (v1).**
41
- - `drawMesh(vertices: List[Vertex], indices: List[U16], texture: U32)`. The host copies the data per call.
41
+ - `drawMesh(vertices: List[Vertex], indices: List[i32], texture: i32)`. The host copies the data per call.
42
- - `loadMaterial(vertexGlsl: Str, fragmentGlsl: Str) -> U32` and `useMaterial(m)` wrap macroquad's `Material`.
42
+ - `loadMaterial(vertexGlsl: Str, fragmentGlsl: Str) -> i32` and `useMaterial(m)` wrap macroquad's `Material`.
43
43
  - Zero-copy linear-memory buffers are deferred until profiling shows a need.
44
44
  8. **Browser host (later).** The JS glue instantiates Plum's wasm-gc module in the browser and forwards its imports to macroquad's own wasm32 build through JS. It is the same extern API, so game code doesn't change.
45
45
  9. **Out of scope:** WebGPU, `StructArray` flat storage, Plum→shader compilation, async and audio streaming.
@@ -52,7 +52,7 @@ Success criteria:
52
52
  ## Global constraints
53
53
 
54
54
  - Run `cargo test --workspace` and `cargo run -q -p plum-cli -- test plum-std plum-examples` after every task, and both must be green.
55
- - typeIds: Bool box 7 (from the built-in Bool work), F32 8, I32 9, U32 10, F32x4 box 11. Declared types still start at 16.
55
+ - typeIds: Bool box 7 (from the built-in Bool work), f32 8, i32 9, i32 10, f32x4 box 11. Declared types still start at 16.
56
56
  - Enable wasmtime `wasm_simd`, `wasm_multi_value` and `wasm_gc` in every host.
57
57
  - The grammar changes (`struct` keyword, operator-trait resolution needs none) are regenerated with `tree-sitter generate --disable-optimizations`.
58
58
 
@@ -60,7 +60,7 @@ Success criteria:
60
60
 
61
61
  ```mermaid
62
62
  flowchart LR
63
- S["game.plum: init/update/draw"] --> C["checker: F32/I32/U32, literal typing, struct, operator traits, F32x4"]
63
+ S["game.plum: init/update/draw"] --> C["checker: f32/i32/i32, literal typing, struct, operator traits, f32x4"]
64
64
  C --> G["codegen: flattened structs, multi-value, v128"]
65
65
  G -->|"game.wasm"| H["plum-game: wasmtime + macroquad loop"]
66
66
  H -->|"extern draw/input/audio"| M["macroquad"]
@@ -75,7 +75,7 @@ flowchart LR
75
75
  T2[literal typing]
76
76
  T3[struct value types]
77
77
  T4[operator traits]
78
- T5[F32x4 + Math.plum]
78
+ T5[f32x4 + Math.plum]
79
79
  T6[plum-game host + Game.plum]
80
80
  T7[pong example + perf checks]
81
81
  T8[browser host]
@@ -91,10 +91,10 @@ flowchart LR
91
91
  ```
92
92
 
93
93
  ### T1 sized numbers
94
- Grammar unchanged, since these are type names. In checker/codegen, add F32/I32/U32 types, arithmetic and comparison per type, casts, boxing ids 8–10, and `Number.plum` methods (`toStr`, `parseF32`…). **Tests:** `f32_precision` (`F32(0.1) + F32(0.2)` prints `0.3` and its f32 bits are exact); `i32_wraps` (`I32(2147483647) + I32(1) == I32(-2147483648)`); `u32_compare_unsigned` (`U32(4000000000) > U32(1)`); `boxed_sized_roundtrip` through `List[F32]`, `Try[U32,[A]]` and `ToStr`.
94
+ Grammar unchanged, since these are type names. In checker/codegen, add f32/i32/i32 types, arithmetic and comparison per type, casts, boxing ids 8–10, and `Number.plum` methods (`toStr`, `parseF32`…). **Tests:** `f32_precision` (`f32(0.1) + f32(0.2)` prints `0.3` and its f32 bits are exact); `i32_wraps` (`i32(2147483647) + i32(1) == i32(-2147483648)`); `u32_compare_unsigned` (`i32(4000000000) > i32(1)`); `boxed_sized_roundtrip` through `List[f32]`, `Try[i32,[A]]` and `ToStr`.
95
95
 
96
96
  ### T2 contextual literal typing
97
- In the checker, an expected sized type flows into literals. In codegen, literals are emitted at that width. **Tests:** `x: F32 = 1.5` gets no Float→F32 error; `drawRect(10, 20, 32, 32)` with F32 params; `v.x * 0.5`; overflow `I32 = 3000000000` is an error naming the type; an unannotated `1.5` is still Float.
97
+ In the checker, an expected sized type flows into literals. In codegen, literals are emitted at that width. **Tests:** `x: f32 = 1.5` gets no Float→f32 error; `drawRect(10, 20, 32, 32)` with f32 params; `v.x * 0.5`; overflow `i32 = 3000000000` is an error naming the type; an unannotated `1.5` is still Float.
98
98
 
99
99
  ### T3 struct value types
100
100
  `struct` keyword in the grammar and parser (`ast::Item::Struct`). Checker: value semantics, no recursion, field assignment on bindings. Codegen: flatten to locals/params, multi-value returns, box at generic and trait edges. **Tests:** `struct_copy_semantics` (`b := a; b.x = 9.0; a.x == 1.0`); `struct_multi_value_return` (`fun mid(a, b: Vec2) -> Vec2`); `struct_in_list_boxes_roundtrip`; `struct_eq_fieldwise`; `recursive_struct_rejected`.
@@ -102,7 +102,7 @@ In the checker, an expected sized type flows into literals. In codegen, literals
102
102
  ### T4 operator traits
103
103
  `Add/Sub/Mul/Div/Neg/Eq` traits in `plum-std/Ops.plum`. The checker maps binary/unary ops on non-primitive operands to those methods, with static dispatch, scalar-RHS overloads, and errors like `Vec2 does not implement Add`. **Tests:** `vec2_add_mul_scalar` (`(Vec2(1,2) + Vec2(3,4)) * 2.0 == Vec2(8, 12)`); `neg`; `missing_op_trait_error`.
104
104
 
105
- ### T5 F32x4 + Math.plum
105
+ ### T5 f32x4 + Math.plum
106
106
  v128 primitive, lane ops, `splat`/`dot`/`shuffle`, boxing id 11. `Math.plum`: Vec2, Vec3, Vec4, Quat, Mat4 (`mul`, `transpose`, `inverse`, `perspective`, `lookAt`, `ortho`, `translate`, `rotate`, `scale`), `lerp`, `clamp`. **Tests:** `mat4_identity_mul`; `mat4_inverse_roundtrip` (within 1e-5); `quat_rotate_90deg` (within 1e-6); `simd_ops_emitted` (the wasm contains `f32x4.mul`).
107
107
 
108
108
  ### T6 plum-game host + Game.plum
@@ -117,5 +117,5 @@ JS glue plus macroquad's wasm32 build, the same `Game.plum` API, and `plum game
117
117
  ## Open questions to settle before running
118
118
 
119
119
  - Will the user's further ideas change the scope or order? This plan is still a draft.
120
- - Should `Float` be renamed to `F64` (with `Int` → `I64`) for consistency, now that sized types exist?
120
+ - Should `Float` be renamed to `f64` (with `Int` → `I64`) for consistency, now that sized types exist?
121
121
  - Should struct field mutation require `var` bindings, or is every binding mutable as today?
docs/plans/2026-10-26-zig-generics-and-numbers.md CHANGED
@@ -4,7 +4,7 @@
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 sized primitives, with literals defaulting to `I32`/`F32` for games.
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.
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.
@@ -17,22 +17,25 @@
17
17
  ## Design
18
18
 
19
19
  What the user decided:
20
- - Integer literals default to `I32` and float literals to `F32`.
20
+ - Integer literals default to `i32` and float literals to `f32`. Later: only native wasm types, lowercase, no pretend widths (no u8/u32).
21
21
  - Traits are removed in favour of Zig-style duck typing, with one copy per type ("Zig-faithful").
22
22
  - Mixed types are handled with enums only (the Zig way).
23
23
  - Comptime covers duck-typed type params only, with no compile-time execution.
24
24
  - The `[T]` syntax is kept.
25
25
  - Reference: axelmagn.com "Zig traits": traits are a convention (records of functions passed explicitly), not a language feature.
26
26
 
27
- 1. **Sized numbers.**
28
- - Primitives: `I8 I16 I32 I64 U8 U16 U32 U64 F32 F64`. `Int`, `Float`, `Byte` and the `Number` enum are removed, with no aliases.
29
- - Literals: an integer literal is `I32` and a float literal is `F32` unless the expected type says otherwise. Contextual typing (game plan item 2) is folded in here. A literal that doesn't fit its type is a compile error.
30
- - Mixed widths never convert implicitly, so `I32 + I64` is an error. Casts are written `I64(x)`, `F32(x)`, etc.
31
- - 8- and 16-bit types are stored as i32 and masked after arithmetic.
32
- - Unsigned types use wasm's unsigned compare/div/rem/shift and `convert_u`.
33
- - Ruling: std uses `I64`/`F64` explicitly where precision matters: `Time` millis, `Json` numbers, `parseI64`/`parseF64`, `Uuid`, `Random`. The cost is that std signatures change, but it avoids silent overflow (i32 millis wrap after about 24 days).
34
- - Per-type `parse*`/`toStr` move to `Str` (`s.parseI32()`, …) and to compiler intrinsics. `Number.plum` is deleted.
35
- 2. **Bool fully built in.** `toStr` becomes a compiler intrinsic, `parse` becomes `Str.parseBool() -> Try[Bool, [BadBoolStr]]`, and `and`/`or` are dropped. `Bool.plum` is deleted.
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 `&&`/`||`.
36
39
  3. **Duck-typed generics.**
37
40
  - 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.
38
41
  - A generic body is type-checked once per concrete instantiation, with `TParam`s substituted, at the point the instantiation is first needed.
@@ -41,12 +44,12 @@ What the user decided:
41
44
  4. **Instantiation (copy per type), on demand.**
42
45
  - The worklist starts at `main`, exported fns and tests. Each call site with concrete type args enqueues `(fn, [types])`.
43
46
  - Each key is checked and compiled once, and memoized.
44
- - Generic types (`List[I32]`, `Try[Str, [A]]`) get one GC struct per distinct instantiation, keyed by the structural type, never by mangled strings.
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
48
  - Only reachable instantiations are produced, which avoids the old "every method for every specialization" problem.
46
- - Names in wasm are only for debugging (`List[I32].map`).
49
+ - Names in wasm are only for debugging (`List[i32].map`).
47
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).
48
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.
49
- 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.
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.
50
53
  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.
51
54
  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.
52
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.
@@ -54,13 +57,13 @@ What the user decided:
54
57
  Success criteria:
55
58
  - `plum test plum-std plum-examples` and `cargo test --workspace` are green.
56
59
  - A typical compile is ≤ 60 ms, against today's 20–25 ms erased. That's the agreed cost of copying per type.
57
- - `List[I32].map` in the wasm touches no boxes.
60
+ - `List[i32].map` in the wasm touches no boxes.
58
61
  - A generic called with a type lacking a method fails with the use-site chain message.
59
62
 
60
63
  ## Global constraints
61
64
 
62
65
  - Both suites are green after every task. Regenerate the grammar only with `--disable-optimizations`.
63
- - 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`.
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`.
64
67
  - No `$` name mangling anywhere. The instantiation key is `(item path, Vec<PlumType>)`.
65
68
 
66
69
  ## Tasks (sequential)
@@ -83,18 +86,23 @@ flowchart LR
83
86
  T6 --> T7
84
87
  ```
85
88
 
86
- ### T1 sized numbers + literal typing
87
- Checker and codegen for the 10 primitives, unsigned ops, masking, casts, contextual literals (default I32/F32), and the mixing error. Tests:
88
- - `u8_wraps`: `U8(255) + U8(1) == U8(0)`.
89
- - `u32_unsigned_div`: `U32(4000000000) / U32(2) == U32(2000000000)`.
90
- - `i32_overflow_wraps`.
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`.
91
93
  - `f32_exact_bits` for `0.1`.
92
- - `literal_defaults`: `x := 5` is I32, `y := 1.5` is F32.
93
- - `mix_error`: `I32 + I64` is rejected.
94
- - `literal_overflow_error`.
95
-
96
- ### T2 Bool/Number std removal
97
- Delete Bool.plum and Number.plum. Add intrinsic `toStr`, `Str.parseBool/parseI32/parseI64/parseF32/parseF64`, and move every `Int`→`I32`/`I64` and `Float`→`F32`/`F64` in plum-std (Time/Json/Uuid/Random get the 64-bit types). Tests: the existing std suites, plus `time_millis_no_wrap` (`nowMillis() > 2^31`).
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`.
98
106
 
99
107
  ### T3 per-instantiation checking
100
108
  Re-check generic bodies per concrete instantiation and produce the use-site chain errors. Bounds are removed from the checker. Tests:
@@ -105,10 +113,10 @@ Re-check generic bodies per concrete instantiation and produce the use-site chai
105
113
 
106
114
  ### T4 on-demand instantiation codegen
107
115
  Worklist from roots, one wasm fn/struct per `(item, types)`, structural keys, unboxed values, typed closures. Tests:
108
- - `one_copy_per_distinct_type`: List[I32] and List[Str] give 2 `map`s, and List[I32] used twice still gives 1.
116
+ - `one_copy_per_distinct_type`: List[i32] and List[Str] give 2 `map`s, and List[i32] used twice still gives 1.
109
117
  - `unreachable_generic_not_emitted`.
110
118
  - `no_boxes_in_list_i32_map` (wasm inspection).
111
- - `generic_tag_payload`: `Try[I32, [Bad(T)]]` works.
119
+ - `generic_tag_payload`: `Try[i32, [Bad(T)]]` works.
112
120
 
113
121
  ### T5 remove traits, dyn, boxing
114
122
  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,8 +131,13 @@ Remove the grammar's `trait`/`implements`/bounds, the checker's trait tables and
123
131
  ### T7 migration, docs, bench
124
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.
125
133
 
134
+ ## Rulings (user said "you decide")
135
+
136
+ - 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.
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.
140
+
126
141
  ## Open questions
127
142
 
128
143
  - Will the user's further ideas change scope or order? This is still a draft.
129
- - What should happen when `List[I32]` and `List[I64]` are both used? That's two copies, which is fine, but should there be a per-generic instantiation-count warning threshold other than 200?
130
- - Should string interpolation `"{x}"` on a type without `toStr` be an error at the use site (duck-typed) or fall back to a default representation?