plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
dd5f31d
— Peter John
2026-10-25T18:54:12+05:30
feat(checker): structural TApp/TParam types with substitute/instantiate
docs/plans/2026-10-25-erased-generics.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Erased generics + dyn dispatch (remove monomorphization)
|
|
2
|
+
|
|
3
|
+
**Goal:** Make the compiler simpler and faster by type-checking generics TypeScript-style and compiling each generic fn/type once. Primitives are boxed only at generic edges. Every class/enum value carries a type id for trait dispatch. List and Array are built into the compiler. `monomorphize.rs` is deleted.
|
|
4
|
+
**Architecture:** tree-sitter → `plum-core` AST (unchanged syntax) → `plum-checker` (structural generic types, no monomorphization) → `plum-wasm-codegen` (erased wasm-gc) → hosts.
|
|
5
|
+
**Tech stack:** Rust workspace, wasm-gc via wasm-encoder, wasmtime.
|
|
6
|
+
**Spec path:** this file, section "Design".
|
|
7
|
+
**Detail:** tests
|
|
8
|
+
**Target-min:** 40
|
|
9
|
+
|
|
10
|
+
## Design
|
|
11
|
+
|
|
12
|
+
What the user decided:
|
|
13
|
+
- Simpler compiler, with types erased like TypeScript.
|
|
14
|
+
- Values are boxed only at generic edges.
|
|
15
|
+
- Per-value vtables ("every type like dyn Trait").
|
|
16
|
+
- List/Map/Array built into the compiler.
|
|
17
|
+
- `[T]` generics kept as they are (Zig-style comptime was rejected).
|
|
18
|
+
|
|
19
|
+
Baseline (release build, scratchpad bench): a typical compile is 380–500 ms, and monomorphization is 80% of it (it runs twice, and the global env is cloned for every expression it infers). `Bool.plum` expands from 316 items to 947.
|
|
20
|
+
|
|
21
|
+
1. **Structural types.** `PlumType` gains `TApp(String, Vec<PlumType>)` (e.g. `List[Int]`) and `TParam(String)` (a rigid generic parameter inside a generic body). `plumTypeFromAst` builds them from `ast::Type.generics`, and mangled `$` names disappear. `TTags` stays the same. `unify` compares `TApp` args pairwise. E stays covariant for tag unions (the subset rule from the Roc errors plan).
|
|
22
|
+
2. **Generic checking without monomorphization.**
|
|
23
|
+
- A generic fn or type body is checked once, with its params as `TParam`.
|
|
24
|
+
- A call site instantiates the callee's scheme with fresh `TVar`s and unifies them with the args, the expected type and the closure return types.
|
|
25
|
+
- Field and variant payload types are substituted from the receiver's `TApp` args.
|
|
26
|
+
- A method's own generics (`map(cb: fn(T) -> U)`) are instantiated per call.
|
|
27
|
+
- A method call on a `TParam` resolves through the param's bounds (`K: Hashable` gives `hash()`). Calling a method with no bound is an error: `'T' has no method 'x' (add a bound)`.
|
|
28
|
+
3. **Trait types (dyn).** A trait name can be used as a type (`s: Shape`, `List[Shape]`). A value of any class/enum that implements every trait method coerces to it, and `checkTraitConformance` reuses its existing per-receiver method table.
|
|
29
|
+
4. **Erased runtime representation.**
|
|
30
|
+
- Every class/enum GC struct gets field 0 `typeId: i32`, unique per declared type and stable in declaration order. `Str` is included.
|
|
31
|
+
- `TParam` and trait types lower to `ref null $Obj`, the common supertype `struct {typeId: i32}`, which every class/enum struct subtypes. `Box[T]`'s `T` field is `ref null $Obj`.
|
|
32
|
+
- Boxing: an `Int`/`Float`/`Byte`/`Bool` flowing into a `TParam` or trait slot (arg, return, field, payload, closure arg/ret) is boxed into `$BoxI64`/`$BoxF64`/`$BoxI32` (each has a typeId header). Where the instantiated type is primitive, reading back casts and unboxes. The existing `$Tag` boxes are reused and get headers too. `Bool` stays an enum ref and needs no box.
|
|
33
|
+
- A generic fn, type or method compiles to exactly one wasm function or struct. A closure passed to a generic param is compiled with erased signatures, and a call site whose concrete closure types are primitive gets a wrapper thunk that boxes and unboxes.
|
|
34
|
+
5. **Trait dispatch.** For each trait method, codegen emits one dispatcher `$dyn$<Trait>$<method>(self: ref $Obj, args…)`. It reads `typeId` and `br_table`s to the concrete method, with boxed primitives dispatching to the primitive's methods (`Int.hash`, `Str.toStr`). Calls on a concrete receiver stay direct calls.
|
|
35
|
+
6. **Built-in collections.** Ruling: List and Array are compiler intrinsics. Map stays Plum code in std on top of them, erased and compiled once, because a hand-written wasm hash table is a lot of codegen for no benefit once Map is no longer specialized.
|
|
36
|
+
- `List[T]` = `struct {typeId, len: i32, data: ref $AnyArray}`, growable with doubling. Its intrinsic methods are `List()`/variadic `List(a, b)`, `len`, `get` (→ `Try[T, [OutOfBounds]]`), `set`, `push`, `pop` (→ `[ListWasEmpty]`) and `clear`.
|
|
37
|
+
- `Array[T]` = the existing `array<anyref>` + `len`/`get`/`set`/`push`.
|
|
38
|
+
- The rest of List's API (map, filter, reduce, each, chunk, toStr…) stays Plum code in `List.plum`, built on the intrinsics. List is no longer a linked list.
|
|
39
|
+
7. **Removed:** `monomorphize.rs`, `monomorphize_tests.rs`, `erasedEnumName`, `tagUnionSegmentsCompatible` and its mangled-name parsing, and every `$`-mangling path in codegen. Comments in std that describe monomorphization workarounds (eager per-specialization compilation, `toTryStr` callbacks) are updated or removed, with behaviour kept the same.
|
|
40
|
+
8. **Out of scope:** generic tag payloads (still non-generic), separate compilation, removing boxing via specialization.
|
|
41
|
+
|
|
42
|
+
Success criteria:
|
|
43
|
+
- All of `plum test plum-std plum-examples` (1951) and `cargo test --workspace` stay green after every task.
|
|
44
|
+
- At the end, a typical compile is ≤ 100 ms and wasm for `Bool.plum` is smaller than 96 KB, measured with `scratchpad/bench`.
|
|
45
|
+
|
|
46
|
+
## Global constraints
|
|
47
|
+
|
|
48
|
+
- Run `cargo test --workspace` and `cargo run -q -p plum-cli -- test plum-std plum-examples` after every task, and both must be green. T1–T3 keep the monomorphizer feeding codegen, so behaviour can't regress before the switch in T4.
|
|
49
|
+
- `touch` edited `.rs` files before trusting cargo. Never run `plum format` on real files. Regenerate tree-sitter only with `--disable-optimizations`, though no grammar change is expected.
|
|
50
|
+
- Type id 0 is reserved for `$Obj` itself. Box type ids: Int=1, Float=2, Byte=3, Tag=4. Declared classes/enums start at 16.
|
|
51
|
+
- Error wording: `'T' has no method 'x' (add a bound)`. `type 'X' does not implement trait 'Y': missing 'm'`.
|
|
52
|
+
|
|
53
|
+
## Main diagram
|
|
54
|
+
|
|
55
|
+
```mermaid
|
|
56
|
+
flowchart LR
|
|
57
|
+
P["parser AST (unchanged)"] --> C["checker: TApp/TParam, instantiate per call, bounds, trait types"]
|
|
58
|
+
C -->|"typed AST + expr types"| G["codegen: erased structs, typeId header, box at generic edges"]
|
|
59
|
+
G --> D["dispatchers: dyn Trait.method via typeId"]
|
|
60
|
+
G --> L["intrinsics: List / Array"]
|
|
61
|
+
L --> S["std: List API + Map in Plum, compiled once"]
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## File structure
|
|
65
|
+
|
|
66
|
+
- `plum-checker/src/types.rs`: TApp/TParam, substitution, instantiation.
|
|
67
|
+
- `plum-checker/src/lib.rs`: generic checking, bounds, trait types.
|
|
68
|
+
- `plum-checker/src/monomorphize.rs`: deleted in T5.
|
|
69
|
+
- `plum-wasm-codegen/src/lib.rs`: erasure, boxing, typeId, dispatchers, intrinsics.
|
|
70
|
+
- `plum-std/List.plum`, `Array.plum`, `Map.plum`: rewritten on the intrinsics.
|
|
71
|
+
- `README.md`: a "How generics compile" section.
|
|
72
|
+
|
|
73
|
+
Ruling: sequential [all] - every task edits plum-checker/src/lib.rs or plum-wasm-codegen/src/lib.rs, and each task's tests compile real Plum through the previous stage - cost: no parallelism.
|
|
74
|
+
|
|
75
|
+
```mermaid
|
|
76
|
+
flowchart LR
|
|
77
|
+
T1[types]
|
|
78
|
+
T2[generic checking]
|
|
79
|
+
T3[trait types + typeId]
|
|
80
|
+
T4[erased codegen switch]
|
|
81
|
+
T5[intrinsics + std + delete mono]
|
|
82
|
+
T6[docs + bench]
|
|
83
|
+
%% taskgraph
|
|
84
|
+
T1 --> T2
|
|
85
|
+
T2 --> T3
|
|
86
|
+
T3 --> T4
|
|
87
|
+
T4 --> T5
|
|
88
|
+
T5 --> T6
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```mermaid
|
|
92
|
+
%% schedule
|
|
93
|
+
gantt
|
|
94
|
+
title Predicted schedule (mm:ss)
|
|
95
|
+
dateFormat X
|
|
96
|
+
axisFormat %M:%S
|
|
97
|
+
section lane 1
|
|
98
|
+
T1 structural types :t1, 0, 118s
|
|
99
|
+
T2 generic checking :t2, 118, 163s
|
|
100
|
+
T3 trait types + typeId :t3, 281, 153s
|
|
101
|
+
T4 erased codegen switch :t4, 434, 166s
|
|
102
|
+
T5 intrinsics std rewrite delete monom :t5, 600, 196s
|
|
103
|
+
T6 docs LSP benchmark :t6, 795, 141s
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
critical path: T1 → T2 → T3 → T4 → T5 → T6 (15.6 min) · peak width 1 · predicted build 15.6 min · predicted total ≈ 19.1 min (target ✓)
|
|
107
|
+
|
|
108
|
+
## Package: checker
|
|
109
|
+
|
|
110
|
+
**App type:** library / evaluator. **Categories:** deep, state.
|
|
111
|
+
**Probe entry:** run=cargo run -q -p plum-cli -- test plum-std plum-examples
|
|
112
|
+
|
|
113
|
+
### T1 structural types · Complexity 5 · Est. context 50k
|
|
114
|
+
**Owns:** plum-checker/src/types.rs, plum-checker/tests/types_tests.rs
|
|
115
|
+
**Components:** 1
|
|
116
|
+
**LOC owned:** 220
|
|
117
|
+
**Tests:** fast
|
|
118
|
+
**Model:** sonnet
|
|
119
|
+
**Est. time:** 3 min
|
|
120
|
+
**Consumes:** none
|
|
121
|
+
**Produces:** PlumType::TApp(String, Vec<PlumType>) · PlumType::TParam(String) · types::substitute(ty: &PlumType, s: &BTreeMap<String, PlumType>) -> PlumType · types::instantiate(params: &[String], ty: &PlumType, fresh: &mut u32) -> (PlumType, BTreeMap<String, PlumType>) · Display: TApp("Try",[TInt,TTags{A}]) -> "Try[Int, [A]]"
|
|
122
|
+
**Review Focus:** `substitute_nested`: substitute(TApp(List,[TApp(Try,[TParam T, TTags{A}])]), {T: Int}) gives List[Try[Int, [A]]]; `instantiate_fresh_distinct`: two instantiations of `fn(T) -> T` produce different TVars.
|
|
123
|
+
**Test code:** Rust unit tests for the two named cases plus Display round-trips.
|
|
124
|
+
**Steps:** 1 failing tests, 2 FAIL, 3 implement, 4 PASS
|
|
125
|
+
**Notes:** Adding the variants breaks exhaustive matches elsewhere. Add minimal compile-only arms there (allowed exception), treating TApp like TNamed of its base name for now, so every existing suite stays green.
|
|
126
|
+
|
|
127
|
+
### T2 generic checking · Complexity 9 · Est. context 80k
|
|
128
|
+
**Owns:** plum-checker/src/lib.rs, plum-checker/tests/checker_tests.rs
|
|
129
|
+
**Components:** 2
|
|
130
|
+
**LOC owned:** 380
|
|
131
|
+
**Tests:** fast
|
|
132
|
+
**Model:** sonnet
|
|
133
|
+
**Est. time:** 6 min
|
|
134
|
+
**Consumes:** PlumType::TApp, PlumType::TParam, types::substitute, types::instantiate (T1)
|
|
135
|
+
**Produces:** plum_checker::checkSource runs on the ORIGINAL source (no monomorphizeSource call inside) · plum_checker::typedSource(source: &ast::Source) -> CheckResult<TypedInfo>, where TypedInfo maps an expr pointer (usize) to its instantiated PlumType, for codegen
|
|
136
|
+
**Review Focus:** (deep) `method_own_generic_instantiated_per_call`: `xs.map(|n| n * 2)` on List[Int] is List[Int], and `xs.map(|n| "{n}")` is List[Str]; `nested_generic_field`: Box[Try[Int,[A]]].value is Try[Int, [A]]; (state) `unbounded_tparam_method_error`: `fun f[T](x: T) -> Int = x.hash()` reports `'T' has no method 'hash' (add a bound)`; `bounded_ok`: with `T: Hashable` it passes; `tags_covariant_in_tapp`: Try[Int,[A]] passes where Try[Int,[A,B]] is expected.
|
|
137
|
+
**Test code:** Rust tests for the named cases. All of plum-std and plum-examples must check with 0 errors on the un-monomorphized source.
|
|
138
|
+
**Steps:** 1 failing tests, 2 FAIL, 3 implement, 4 PASS
|
|
139
|
+
**Notes:** Codegen still calls monomorphizeSource itself, so runtime behaviour doesn't change. Keep the old mangled-name checker paths only where codegen still calls into them, through `inferExpr` on monomorphized source.
|
|
140
|
+
|
|
141
|
+
### T3 trait types + typeId · Complexity 7 · Est. context 75k
|
|
142
|
+
**Owns:** plum-checker/src/lib.rs, plum-checker/tests/checker_tests.rs, plum-wasm-codegen/src/lib.rs, plum-wasm-codegen/tests/codegen_tests.rs
|
|
143
|
+
**Components:** 2
|
|
144
|
+
**LOC owned:** 300
|
|
145
|
+
**Tests:** fast
|
|
146
|
+
**Model:** sonnet
|
|
147
|
+
**Est. time:** 5.5 min
|
|
148
|
+
**Consumes:** plum_checker::checkSource (T2)
|
|
149
|
+
**Produces:** `$Obj {typeId: i32}` supertype with typeId field 0 on every class/enum struct · dispatcher fns `$dyn$<Trait>$<method>` · trait names usable as types (checker coercion + codegen)
|
|
150
|
+
**Review Focus:** (deep) `heterogeneous_list_dispatch`: `List[Shape]` holding Circle and Square, summed `area()` is exact; (state) `missing_trait_method_rejected`: coercing a type without `area` reports `type 'X' does not implement trait 'Shape': missing 'area'`; `boxed_int_dispatch`: an Int passed as `ToStr` gives "5" from toStr().
|
|
151
|
+
**Test code:** codegen tests that run through wasmtime for the three named cases, plus one example `plum-examples/traits.plum` test block (Owns: add plum-examples/traits.plum).
|
|
152
|
+
**Steps:** 1 failing tests, 2 FAIL, 3 implement, 4 PASS
|
|
153
|
+
**Notes:** Monomorphization is still on here, and trait-typed values work under it, because a dyn value isn't generic.
|
|
154
|
+
|
|
155
|
+
## Package: codegen
|
|
156
|
+
|
|
157
|
+
**App type:** library / evaluator. **Categories:** deep, numeric, state.
|
|
158
|
+
**Probe entry:** run=cargo run -q -p plum-cli -- test plum-std plum-examples
|
|
159
|
+
|
|
160
|
+
### T4 erased codegen switch · Complexity 9 · Est. context 80k
|
|
161
|
+
**Owns:** plum-wasm-codegen/src/lib.rs, plum-wasm-codegen/tests/codegen_tests.rs, plum-cli/src/main.rs
|
|
162
|
+
**Components:** 2
|
|
163
|
+
**LOC owned:** 400
|
|
164
|
+
**Tests:** fast
|
|
165
|
+
**Model:** sonnet
|
|
166
|
+
**Est. time:** 6 min
|
|
167
|
+
**Consumes:** plum_checker::typedSource(source) -> CheckResult<TypedInfo> (T2) · `$Obj` header and dispatchers (T3)
|
|
168
|
+
**Produces:** compileSource(source) compiles the un-monomorphized AST: one wasm fn per generic fn or method, TParam → ref null $Obj, primitive boxing and unboxing at generic edges, closure wrapper thunks
|
|
169
|
+
**Review Focus:** (numeric) `boxed_i64_extremes_through_generic`: identity[T] on i64::MIN, 2^53+1 and -0.0 Float round-trips exactly; (deep) `generic_closure_thunk`: `List(1,2,3).map(|n| n * 2)` sums to 12; `try_of_list_of_try`: Try[List[Try[Int,[A]]],[B]] round-trips; (state) `one_copy_per_generic`: a module using List[Int] and List[Str] has exactly one compiled `map` function.
|
|
170
|
+
**Test code:** codegen tests for the named cases. Both full plum suites green.
|
|
171
|
+
**Steps:** 1 failing tests, 2 FAIL, 3 implement, 4 PASS
|
|
172
|
+
**Notes:** Stop calling monomorphizeSource in codegen and checkSource. Leave monomorphize.rs on disk, unused, for T5 to delete. List/Map are still the Plum std code here, now compiled erased.
|
|
173
|
+
|
|
174
|
+
### T5 intrinsics, std rewrite, delete monomorphizer · Complexity 7 · Est. context 75k
|
|
175
|
+
**Owns:** plum-wasm-codegen/src/lib.rs, plum-wasm-codegen/tests/codegen_tests.rs, plum-std/, plum-checker/src/monomorphize.rs, plum-checker/tests/monomorphize_tests.rs, plum-checker/src/lib.rs
|
|
176
|
+
**Components:** 2
|
|
177
|
+
**LOC owned:** 350
|
|
178
|
+
**Tests:** slow
|
|
179
|
+
**Model:** sonnet
|
|
180
|
+
**Est. time:** 6 min
|
|
181
|
+
**Consumes:** erased compileSource (T4)
|
|
182
|
+
**Produces:** List/Array intrinsics (Design item 6) · List.plum API on the intrinsics (no linked list) · Map.plum on erased generics with Hashable dispatch · monomorphize.rs and monomorphize_tests.rs deleted, plus dead mangled-name helpers
|
|
183
|
+
**Review Focus:** (state) `list_get_set_push_pop`: push 1000, get(999) is Ok(999), pop empties, get(0) on an empty list is Err(OutOfBounds); (numeric) `list_growth`: len stays exact across the doubling boundaries 16/17 and 1024/1025; (deep) `map_collisions_erased`: Map[Str,Int] with 500 keys round-trips.
|
|
184
|
+
**Test code:** `test` blocks in List.plum/Map.plum for the named cases. All existing std tests still green.
|
|
185
|
+
**Steps:** 1 failing tests, 2 FAIL, 3 implement, 4 PASS
|
|
186
|
+
|
|
187
|
+
### T6 docs, LSP, benchmark · Complexity 3 · Est. context 40k
|
|
188
|
+
**Owns:** README.md, plum-lsp/src/, plum-examples/, docs/plans/2026-10-25-erased-generics.md
|
|
189
|
+
**Components:** 1
|
|
190
|
+
**LOC owned:** 120
|
|
191
|
+
**Tests:** slow
|
|
192
|
+
**Model:** sonnet
|
|
193
|
+
**Est. time:** 2.5 min
|
|
194
|
+
**Consumes:** final compiler (T5)
|
|
195
|
+
**Produces:** a README "How generics compile" section (erasure, boxing at edges, dyn traits, built-in List/Array), the bench results table appended to this plan, and the LSP compiling against the new checker API
|
|
196
|
+
**Review Focus:** `bench_targets`: a typical compile ≤ 100 ms and the Bool.plum wasm < 96 KB, or the numbers recorded with the reason they missed.
|
|
197
|
+
**Test code:** plum test suites green. The bench binary at the scratchpad path is rebuilt against the workspace.
|
|
198
|
+
**Steps:** 1 run the bench, 2 write docs, 3 verify
|
|
199
|
+
|
|
200
|
+
## Review Focus
|
|
201
|
+
|
|
202
|
+
Checker: deep (nested TApp substitution, per-call method generics), state (bounds, trait coercion). Codegen: numeric (boxing precision), deep (closure thunks, nested erased generics), state (one copy per generic, typeId stability).
|
|
203
|
+
|
|
204
|
+
## Self-review
|
|
205
|
+
|
|
206
|
+
Design items 1→T1, 2→T2, 3,5→T3, 4→T4, 6,7→T5, success criteria→T6. There are no placeholders. Each Consumes line matches a Produces line. Tasks are sequential by ruling, and each one leaves both suites green.
|
plum-checker/src/monomorphize.rs
CHANGED
|
@@ -150,7 +150,8 @@ fn plumTypeToAstType(t: &PlumType) -> ast::Type {
|
|
|
150
150
|
PlumType::TByte => "Byte".to_string(),
|
|
151
151
|
PlumType::TByteSlice => "[]Byte".to_string(),
|
|
152
152
|
PlumType::TUnit => "Unit".to_string(),
|
|
153
|
+
PlumType::TNamed(n) | PlumType::TApp(n, _) => n.clone(),
|
|
153
|
-
PlumType::
|
|
154
|
+
PlumType::TParam(n) => n.clone(),
|
|
154
155
|
PlumType::TVar(_)
|
|
155
156
|
| PlumType::TFun(_, _)
|
|
156
157
|
| PlumType::TVariadic(_)
|
plum-checker/src/types.rs
CHANGED
|
@@ -26,6 +26,10 @@ pub enum PlumType {
|
|
|
26
26
|
/// `Display` is the canonical string (sorted, no spaces) the parser also
|
|
27
27
|
/// produces for the type annotation, so mangled names round-trip.
|
|
28
28
|
TTags(BTreeMap<String, Option<Box<PlumType>>>),
|
|
29
|
+
/// A generic type applied to arguments, e.g. `List[Int]` -> `TApp("List", [TInt])`.
|
|
30
|
+
TApp(String, Vec<PlumType>),
|
|
31
|
+
/// A declared generic parameter (`T`) inside its own generic item's signature/body.
|
|
32
|
+
TParam(String),
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
impl std::fmt::Display for PlumType {
|
|
@@ -45,6 +49,11 @@ impl std::fmt::Display for PlumType {
|
|
|
45
49
|
}
|
|
46
50
|
PlumType::TNamed(n) => write!(f, "{}", n),
|
|
47
51
|
PlumType::TVariadic(inner) => write!(f, "...{}", inner),
|
|
52
|
+
PlumType::TParam(n) => write!(f, "{}", n),
|
|
53
|
+
PlumType::TApp(n, args) => {
|
|
54
|
+
let a: Vec<_> = args.iter().map(|p| p.to_string()).collect();
|
|
55
|
+
write!(f, "{}[{}]", n, a.join(", "))
|
|
56
|
+
}
|
|
48
57
|
PlumType::TTags(tags) => {
|
|
49
58
|
let parts: Vec<String> = tags
|
|
50
59
|
.iter()
|
|
@@ -59,6 +68,41 @@ impl std::fmt::Display for PlumType {
|
|
|
59
68
|
}
|
|
60
69
|
}
|
|
61
70
|
|
|
71
|
+
/// Replace every `TParam(name)` found in `s`, recursing through all structure.
|
|
72
|
+
pub fn substitute(ty: &PlumType, s: &BTreeMap<String, PlumType>) -> PlumType {
|
|
73
|
+
use PlumType::*;
|
|
74
|
+
match ty {
|
|
75
|
+
TParam(n) => s.get(n).cloned().unwrap_or_else(|| ty.clone()),
|
|
76
|
+
TApp(n, args) => TApp(n.clone(), args.iter().map(|a| substitute(a, s)).collect()),
|
|
77
|
+
TFun(ps, r) => TFun(
|
|
78
|
+
ps.iter().map(|p| substitute(p, s)).collect(),
|
|
79
|
+
Box::new(substitute(r, s)),
|
|
80
|
+
),
|
|
81
|
+
TVariadic(i) => TVariadic(Box::new(substitute(i, s))),
|
|
82
|
+
TTags(tags) => TTags(
|
|
83
|
+
tags.iter()
|
|
84
|
+
.map(|(k, v)| (k.clone(), v.as_ref().map(|t| Box::new(substitute(t, s)))))
|
|
85
|
+
.collect(),
|
|
86
|
+
),
|
|
87
|
+
_ => ty.clone(),
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// Instantiate `params` with fresh type variables (`_t<N>`, `fresh` is the shared counter)
|
|
92
|
+
/// and return the substituted type plus the param -> fresh-var map.
|
|
93
|
+
pub fn instantiate(
|
|
94
|
+
params: &[String],
|
|
95
|
+
ty: &PlumType,
|
|
96
|
+
fresh: &mut u32,
|
|
97
|
+
) -> (PlumType, BTreeMap<String, PlumType>) {
|
|
98
|
+
let mut s = BTreeMap::new();
|
|
99
|
+
for p in params {
|
|
100
|
+
s.insert(p.clone(), PlumType::TVar(format!("_t{}", *fresh)));
|
|
101
|
+
*fresh += 1;
|
|
102
|
+
}
|
|
103
|
+
(substitute(ty, &s), s)
|
|
104
|
+
}
|
|
105
|
+
|
|
62
106
|
#[derive(Debug, Clone, PartialEq)]
|
|
63
107
|
pub struct TypeScheme {
|
|
64
108
|
pub vars: Vec<String>,
|
plum-checker/tests/types_tests.rs
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
use plum_checker::types::{instantiate, substitute, PlumType::*};
|
|
2
|
+
use std::collections::BTreeMap;
|
|
3
|
+
|
|
4
|
+
fn tags_a() -> plum_checker::types::PlumType {
|
|
5
|
+
let mut m = BTreeMap::new();
|
|
6
|
+
m.insert("A".to_string(), None);
|
|
7
|
+
TTags(m)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
#[test]
|
|
11
|
+
fn substitute_nested() {
|
|
12
|
+
let ty = TApp(
|
|
13
|
+
"List".into(),
|
|
14
|
+
vec![TApp("Try".into(), vec![TParam("T".into()), tags_a()])],
|
|
15
|
+
);
|
|
16
|
+
let mut s = BTreeMap::new();
|
|
17
|
+
s.insert("T".to_string(), TInt);
|
|
18
|
+
let out = substitute(&ty, &s);
|
|
19
|
+
assert_eq!(out.to_string(), "List[Try[Int, [A]]]");
|
|
20
|
+
assert_eq!(
|
|
21
|
+
out,
|
|
22
|
+
TApp("List".into(), vec![TApp("Try".into(), vec![TInt, tags_a()])])
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#[test]
|
|
27
|
+
fn instantiate_fresh_distinct() {
|
|
28
|
+
let ty = TFun(vec![TParam("T".into())], Box::new(TParam("T".into())));
|
|
29
|
+
let mut fresh = 0u32;
|
|
30
|
+
let (a, sa) = instantiate(&["T".to_string()], &ty, &mut fresh);
|
|
31
|
+
let (b, sb) = instantiate(&["T".to_string()], &ty, &mut fresh);
|
|
32
|
+
assert_ne!(a, b);
|
|
33
|
+
assert_ne!(sa["T"], sb["T"]);
|
|
34
|
+
match (&a, &sa["T"]) {
|
|
35
|
+
(TFun(ps, r), v @ TVar(_)) => {
|
|
36
|
+
assert_eq!(&ps[0], v);
|
|
37
|
+
assert_eq!(&**r, v);
|
|
38
|
+
}
|
|
39
|
+
_ => panic!("unexpected {:?}", a),
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#[test]
|
|
44
|
+
fn display_round_trips() {
|
|
45
|
+
let t = TApp("Try".into(), vec![TInt, tags_a()]);
|
|
46
|
+
assert_eq!(t.to_string(), "Try[Int, [A]]");
|
|
47
|
+
assert_eq!(TParam("T".into()).to_string(), "T");
|
|
48
|
+
assert_eq!(
|
|
49
|
+
TApp("Map".into(), vec![TStr, TApp("List".into(), vec![TInt])]).to_string(),
|
|
50
|
+
"Map[Str, List[Int]]"
|
|
51
|
+
);
|
|
52
|
+
}
|
plum-wasm-codegen/src/lib.rs
CHANGED
|
@@ -770,6 +770,7 @@ fn pushSelfPlaceholder(vt: ValType, body: &mut Vec<u8>) {
|
|
|
770
770
|
|
|
771
771
|
fn plumTypeToValtype(t: &PlumType) -> ValType {
|
|
772
772
|
match t {
|
|
773
|
+
PlumType::TApp(n, _) => plumTypeToValtype(&PlumType::TNamed(n.clone())),
|
|
773
774
|
PlumType::TInt => ValType::I64,
|
|
774
775
|
PlumType::TFloat => ValType::F64,
|
|
775
776
|
PlumType::TBool => withGcTypes(|r| {
|
|
@@ -819,7 +820,7 @@ fn plumTypeToValtype(t: &PlumType) -> ValType {
|
|
|
819
820
|
// existing "not modeled as a concrete shape here" treatment.
|
|
820
821
|
PlumType::TTags(_) => withGcTypes(|r| gcRef(r.tag_type_idx)),
|
|
821
822
|
PlumType::TFun(_, _) | PlumType::TVariadic(_) => ValType::Ref(RefType::ANYREF),
|
|
822
|
-
PlumType::TVar(_) | PlumType::TUnit => ValType::I64,
|
|
823
|
+
PlumType::TVar(_) | PlumType::TParam(_) | PlumType::TUnit => ValType::I64,
|
|
823
824
|
}
|
|
824
825
|
}
|
|
825
826
|
|
|
@@ -884,6 +885,7 @@ fn isArraySpecialization(name: &str) -> bool {
|
|
|
884
885
|
/// structure: this function is only ever called during its second pass).
|
|
885
886
|
fn plumTypeToGcValtype(t: &PlumType, registry: &GcTypeRegistry) -> ValType {
|
|
886
887
|
match t {
|
|
888
|
+
PlumType::TApp(n, _) => plumTypeToGcValtype(&PlumType::TNamed(n.clone()), registry),
|
|
887
889
|
PlumType::TInt => ValType::I64,
|
|
888
890
|
PlumType::TFloat => ValType::F64,
|
|
889
891
|
PlumType::TBool => {
|
|
@@ -933,7 +935,7 @@ fn plumTypeToGcValtype(t: &PlumType, registry: &GcTypeRegistry) -> ValType {
|
|
|
933
935
|
// yet-meaningful placeholder in the meantime, since nothing consumes it yet.
|
|
934
936
|
PlumType::TTags(_) => gcRef(registry.tag_type_idx),
|
|
935
937
|
PlumType::TFun(_, _) | PlumType::TVariadic(_) => ValType::Ref(RefType::ANYREF),
|
|
936
|
-
PlumType::TVar(_) | PlumType::TUnit => ValType::I64,
|
|
938
|
+
PlumType::TVar(_) | PlumType::TParam(_) | PlumType::TUnit => ValType::I64,
|
|
937
939
|
}
|
|
938
940
|
}
|
|
939
941
|
|