plum

#treesitter#compiler#wasm

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::TNamed(n) => n.clone(),
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