plum
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
|
|
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.** `
|
|
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 `
|
|
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 `
|
|
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:
|
|
23
|
-
3. **Value structs.** `struct Vec2(x:
|
|
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.** `
|
|
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:
|
|
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:
|
|
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) ->
|
|
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:
|
|
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[
|
|
41
|
+
- `drawMesh(vertices: List[Vertex], indices: List[i32], texture: i32)`. The host copies the data per call.
|
|
42
|
-
- `loadMaterial(vertexGlsl: Str, fragmentGlsl: Str) ->
|
|
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),
|
|
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:
|
|
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[
|
|
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
|
|
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:
|
|
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
|
|
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 `
|
|
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
|
|
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 `
|
|
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. **
|
|
28
|
-
- Primitives
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
|
|
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[
|
|
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[
|
|
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:
|
|
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[
|
|
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
|
|
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
|
|
87
|
-
|
|
88
|
-
- `
|
|
89
|
-
- `
|
|
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
|
|
93
|
-
- `mix_error`: `
|
|
94
|
-
- `literal_overflow_error`.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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[
|
|
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[
|
|
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?
|