plum

#treesitter#compiler#wasm

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

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


bf5c846 — Peter John 2026-10-26T10:34:24+05:30
docs(plans): draft Zig-style generics + sized numbers plan; link from game plan
docs/plans/2026-10-26-zig-generics-and-numbers.md ADDED
@@ -0,0 +1,130 @@
1
+ # Zig-style generics, no traits, sized numbers
2
+
3
+ **Status:** draft. Planned only and not run; the user has more ideas to fold in first.
4
+ **Goal:**
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
+ - Use enums for mixed-type values.
7
+ - Replace `Int`/`Float`/`Byte`/`Number` with sized primitives, with literals defaulting to `I32`/`F32` for games.
8
+ - Move Bool's remaining methods into the compiler.
9
+
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
+ **Detail:** tests
12
+ **Relation to other plans:**
13
+ - This plan replaces Design item 1 and task T1 of `2026-10-26-game-engine.md` (sized numbers).
14
+ - It changes that plan's T4: operator traits become duck-typed operator methods.
15
+ - 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
+
17
+ ## Design
18
+
19
+ What the user decided:
20
+ - Integer literals default to `I32` and float literals to `F32`.
21
+ - Traits are removed in favour of Zig-style duck typing, with one copy per type ("Zig-faithful").
22
+ - Mixed types are handled with enums only (the Zig way).
23
+ - Comptime covers duck-typed type params only, with no compile-time execution.
24
+ - The `[T]` syntax is kept.
25
+ - Reference: axelmagn.com "Zig traits": traits are a convention (records of functions passed explicitly), not a language feature.
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.
36
+ 3. **Duck-typed generics.**
37
+ - 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
+ - A generic body is type-checked once per concrete instantiation, with `TParam`s substituted, at the point the instantiation is first needed.
39
+ - Errors point at the use site and include the chain, in the form `in max[Vec2] (called at game.plum:12): Vec2 has no operator '<'`.
40
+ - A generic body that is never instantiated is only syntax-checked plus name-resolved, as in Zig.
41
+ 4. **Instantiation (copy per type), on demand.**
42
+ - The worklist starts at `main`, exported fns and tests. Each call site with concrete type args enqueues `(fn, [types])`.
43
+ - 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.
45
+ - 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`).
47
+ - 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
+ 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.
50
+ 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
+ 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
+ 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
+
54
+ Success criteria:
55
+ - `plum test plum-std plum-examples` and `cargo test --workspace` are green.
56
+ - 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.
58
+ - A generic called with a type lacking a method fails with the use-site chain message.
59
+
60
+ ## Global constraints
61
+
62
+ - 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`.
64
+ - No `$` name mangling anywhere. The instantiation key is `(item path, Vec<PlumType>)`.
65
+
66
+ ## Tasks (sequential)
67
+
68
+ ```mermaid
69
+ flowchart LR
70
+ T1[sized numbers + literal typing]
71
+ T2[Bool/Number std removal]
72
+ T3[per-instantiation checking]
73
+ T4[on-demand instantiation codegen]
74
+ T5[remove traits, dyn, boxing]
75
+ T6[enums for mixed types + operators]
76
+ T7[std/examples migration + docs + bench]
77
+ %% taskgraph
78
+ T1 --> T2
79
+ T2 --> T3
80
+ T3 --> T4
81
+ T4 --> T5
82
+ T5 --> T6
83
+ T6 --> T7
84
+ ```
85
+
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`.
91
+ - `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`).
98
+
99
+ ### T3 per-instantiation checking
100
+ Re-check generic bodies per concrete instantiation and produce the use-site chain errors. Bounds are removed from the checker. Tests:
101
+ - `duck_ok`: `max[Vec2]` works when Vec2 has `lt`.
102
+ - `duck_missing_method_chain`: the message includes `called at`.
103
+ - `uninstantiated_generic_not_type_errored`.
104
+ - `nested_instantiation_chain`: two levels deep.
105
+
106
+ ### T4 on-demand instantiation codegen
107
+ 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.
109
+ - `unreachable_generic_not_emitted`.
110
+ - `no_boxes_in_list_i32_map` (wasm inspection).
111
+ - `generic_tag_payload`: `Try[I32, [Bad(T)]]` works.
112
+
113
+ ### T5 remove traits, dyn, boxing
114
+ 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.
115
+
116
+ ### T6 enums for mixed types + duck-typed operators
117
+ `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:
118
+ - `shape_enum_area_sum`.
119
+ - `vec2_add_operator`.
120
+ - `operator_missing_method_error`.
121
+ - `num_record_passed_explicitly` (the article's pattern, e.g. `sumWith(nums, i32Num)`).
122
+
123
+ ### T7 migration, docs, bench
124
+ 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
+
126
+ ## Open questions
127
+
128
+ - 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?