plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
84e946f
— Peter 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
|
|