plum
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?
|