plum

#treesitter#compiler#wasm

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

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


84e946fPeter John 2026-07-20T20:03:11+05:30
docs: amend generic-enum spec with bare-generic-typed-param fix
docs/superpowers/specs/2026-07-20-generic-enum-multi-instantiation-design.md CHANGED
@@ -51,6 +51,58 @@ machinery (the `enum_variant_owner` ownership table, and the error path it guard
51
51
  unnecessary and should be removed — there is no more collision to detect, since every
52
52
  specialization's variants live under their own unique mangled names.
53
53
 
54
+ ## Addendum: ordinary functions with a bare generic-typed param must be specialized too
55
+
56
+ Implementation surfaced a real, blocking gap the above design didn't account for, discovered
57
+ because it broke the **pre-existing** single-instantiation codegen test: an *ordinary* function
58
+ (one with no lowercase-letter generic params) that takes a bare generic-enum-typed parameter — the
59
+ completely normal way to write this, e.g.
60
+
61
+ ```plum
62
+ unwrapOr(o: Option, default: Int) -> Int =
63
+ match o
64
+ Some(v) => v
65
+ None => default
66
+ ```
67
+
68
+ — never has its own param type resolved to a concrete specialization at all. `o`'s declared type
69
+ stays the literal, unmangled `Option`, so inside `unwrapOr`'s body, `match o` infers a subject
70
+ type of `TNamed("Option")` — a key that can never match the mangling table (keyed by the mangled
71
+ name, `"Option$Int"`). This isn't an ordering bug (verified directly, including by reordering
72
+ source); it's that ordinary functions are never treated as needing specialization at all today,
73
+ even though a bare generic-enum-typed param makes them behave exactly like a specialization
74
+ target. The same root cause almost certainly affects generic **classes** too, for the identical
75
+ shape (`f(b: Box) -> Int = ...`) — untested before now only because no existing test happened to
76
+ exercise it, but the failure mode is analogous (post-monomorphization, the bare generic class
77
+ `Box` no longer exists in the output at all, since only its mangled specializations survive).
78
+
79
+ **Fix, symmetric for both classes and enums:** treat an ordinary function whose param or return
80
+ type bare-names a generic class or enum as needing its own per-call-site specialization, exactly
81
+ mirroring how a truly-generic function (lowercase-letter params) is already specialized:
82
+
83
+ - **Return position** is half-solved already: `maybe_rewrite_return` already resolves a bare
84
+ generic-*class* return (e.g. `-> Box`) from the body's inferred tail type, via the existing
85
+ `self.classes_generic.contains_key(&rt.name)` check — it just never checked the enum registry.
86
+ Add the parallel `self.enums_generic_by_name.contains_key(&rt.name)` check alongside it.
87
+ - **Param position** (the actual blocker) is new: detect, for an otherwise-ordinary function,
88
+ every param whose declared type bare-names a generic class or enum (a new `fn_bare_generic_refs`
89
+ helper, parallel in spirit to `fn_generic_params` but checking against the classes/enums
90
+ registries instead of the lowercase-letter convention). Such a function is deferred (not passed
91
+ through directly) and resolved at each call site: infer the calling argument's already-rewritten
92
+ concrete type (e.g. the first argument to `unwrapOr(Some(13), 0)` is, after its own construction
93
+ site is rewritten, `TNamed("Option$Int")`), bind `{"Option": TNamed("Option$Int")}` into a
94
+ `Substitution`, mangle the function itself (`mangle("unwrapOr", &[TNamed("Option$Int")])` ->
95
+ `"unwrapOr$Option$Int"`), and specialize it via the **existing, unmodified** `specialize_fn` —
96
+ its substitution mechanism already walks and replaces any type whose bare name matches a
97
+ substitution key, so no changes to `specialize_fn` itself are needed, only to how the
98
+ substitution is discovered and populated for this new case.
99
+
100
+ Scoped to free functions only for this fix — a **method** introducing this same shape (a method
101
+ on a non-generic class whose own param bare-names a generic class/enum) is a further edge case,
102
+ consistent with the existing "a method introducing its own additional generic parameter" already
103
+ being out of scope. A function that is simultaneously truly-generic (lowercase letters) *and*
104
+ bare-references another generic type is also out of scope for now (no current example needs it).
105
+
54
106
  ## Testing plan
55
107
 
56
108
  - **Checker tests**: a generic `Option`-shaped enum instantiated at two different concrete types
@@ -65,6 +117,12 @@ specialization's variants live under their own unique mangled names.
65
117
  - Remove or repurpose the now-obsolete "multi-instantiation collision is a clear error" test from
66
118
  the prior plan, since that behavior is being replaced by "multi-instantiation now works
67
119
  correctly" — replace it with the new coexistence test above.
120
+ - The exact regression that surfaced the addendum's gap: an *ordinary* function taking a bare
121
+ generic-enum-typed parameter (`unwrapOr(o: Option, default: Int) -> Int`), compiled and run via
122
+ `wasmtime`, at both a single instantiation (proving no regression to the already-working case)
123
+ and multiple coexisting instantiations. Add the analogous test for a generic **class** (an
124
+ ordinary function taking a bare generic-class-typed parameter, e.g. `sumBox(b: Box) -> Int`),
125
+ since the addendum's fix is symmetric and this shape was never tested before.
68
126
 
69
127
  ## Out of scope
70
128