plum

#treesitter#compiler#wasm

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

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


accb2b1Peter John 2026-09-03T14:49:22+05:30
fix(plum-checker): substitute a generic method's own extra generics in its body; ship Map.map
Files changed (3) hide show
  1. README.md +1 -1
  2. libs/std/map.plum +20 -30
  3. plum-checker/src/monomorphize.rs +137 -1
README.md CHANGED
@@ -390,5 +390,5 @@ Some things parse and type-check but don't compile to wasm yet — `plum-wasm-co
390
390
 
391
391
  - interpolating a `Float` value in a string (`Str`/`Int`/`Bool` interpolation, and plain non-interpolated literals, all compile) — correct decimal formatting of a float is a substantial separate undertaking (something like Grisu/Ryu), scoped out for now
392
392
  - The generic-field-type gap that used to block `libs/std`'s real `List[T]`/`Node[T]` from compiling at all (a class/enum-variant field declared with a concrete instantiation of another generic type, e.g. `Node.next: Option[Node]`) is fixed — `List[T]`'s methods (`get`/`length`/`add`/`set`/`removeAt`/`remove`/`clear`/`reverse`/`each`/`map`/`reduce`/`sort`/`join`, including its `Stringable`-bounded dispatch) are exercised directly, at their real generic type, by the `test` blocks at the bottom of [`libs/std/list.plum`](libs/std/list.plum) itself (run via `plum test libs/std/list.plum`) — not a non-generic stand-in.
393
- - `Map[K, V]` previously hit a *different* bug here: a top-level function whose own declared return type was a fully-concrete generic instantiation (e.g. `fun makeMap() -> Map[Str, Int] = ...`) caused monomorphization to eagerly (and incorrectly) attempt to specialize an unrelated generic type's method (`Option.filter`) at an unresolved type variable, producing a spurious `expected Option, found Option$V` type error even though `filter` was never called anywhere in the program. Root cause: `maybeRewriteReturn` (in `plum-checker/src/monomorphize.rs`) treated ANY return type merely NAMING a known generic class/enum as still-unresolved and in need of rewriting from the body's inferred tail type — even when that name already carried concrete type args (`List[Str]`, `Map[Str, Int]`), clobbering an already-correct declared return type with a stale, unmangled one inferred through tables monomorphization hadn't finished refreshing yet. Fixed by only treating a BARE reference (no `[...]` at all, e.g. `-> Box`) as needing that rewrite; a companion fix pre-resolves every ordinary (non-generic) fn/method's signature into the type tables before any body rewriting begins, so cross-file call order no longer matters. `Option.andThen`/`Result.map`/`Result.mapErr` (a single extra generic, possibly nested inside a closure's return type) are now implemented and working. `Map`'s `map` method specifically (needing TWO new generic params nested inside a closure's return type, on a class rather than an enum) remains genuinely unimplemented a distinct pair of gaps (a generic method's body never gets its own extra generics substituted, and closure-argument inference doesn't use the callee's declared param types as a hint) documented in code and project memory
393
+ - `Map[K, V]` previously hit a *different* bug here: a top-level function whose own declared return type was a fully-concrete generic instantiation (e.g. `fun makeMap() -> Map[Str, Int] = ...`) caused monomorphization to eagerly (and incorrectly) attempt to specialize an unrelated generic type's method (`Option.filter`) at an unresolved type variable, producing a spurious `expected Option, found Option$V` type error even though `filter` was never called anywhere in the program. Root cause: `maybeRewriteReturn` (in `plum-checker/src/monomorphize.rs`) treated ANY return type merely NAMING a known generic class/enum as still-unresolved and in need of rewriting from the body's inferred tail type — even when that name already carried concrete type args (`List[Str]`, `Map[Str, Int]`), clobbering an already-correct declared return type with a stale, unmangled one inferred through tables monomorphization hadn't finished refreshing yet. Fixed by only treating a BARE reference (no `[...]` at all, e.g. `-> Box`) as needing that rewrite; a companion fix pre-resolves every ordinary (non-generic) fn/method's signature into the type tables before any body rewriting begins, so cross-file call order no longer matters. `Option.andThen`/`Result.map`/`Result.mapErr` (a single extra generic, possibly nested inside a closure's return type) are now implemented and working. `Map.map` (needing TWO new generic params nested inside a closure's return type, on a class rather than an enum) is now implemented too, once `specializeFn` was fixed to also substitute a method's own extra generics inside its BODY (not just its params/return) — an explicit bracketed construction like `Map[X, Y](items: ...)` written directly in a method's own body previously kept the literal letters "X"/"Y" forever, silently minting bogus specializations that corrupted unrelated template compilation elsewhere in the program. A second, narrower gap remains: a closure-literal argument whose body does field access on its own param (`|p| cb(p.key, p.val)`) still can't type-check once more than one specialization of the same generic class shares that field name (e.g. both `Pair$Str$Int` and `Pair$Str$Str` declare `key`/`val`) — the checker's field-usage heuristic for inferring a closure param's class becomes ambiguous and gives up; `Map.map`'s own body sidesteps this by using a manual linked-list walk (matching `keys`/`values`/`each`'s existing style) instead of `List.map` with a field-accessing closure
394
394
  - A generic class/enum method whose return type nests that SAME class/enum inside itself (`List[T].chunk(self) -> List[List[T]]`) used to stack-overflow the compiler for every program merely importing the class, whether or not anything called the method — fixed; `List.chunk`/`List.partition` now ship and work as a working example of the pattern (build any inner `List[T]` value via an EXISTING ordinary method, e.g. `self.sublist(...)`, never a second bare `List(...)` construction in the same body, which is ambiguous with the method's own return-type fallback)
libs/std/map.plum CHANGED
@@ -138,36 +138,17 @@ type Map[K, V] =
138
138
  break
139
139
  return result
140
140
 
141
- # `map` is left unimplemented two DISTINCT compiler gaps were found
141
+ fun map(self, cb: fn(K, V) -> Pair[X, Y]) -> Map[X, Y] =
142
- # while attempting it (2026-09-02), both real and both deeper than this
142
+ result := Map[X, Y](items: List[Pair[X, Y]](head: None, tail: None, size: 0))
143
+ current := self.items.head
144
+ while current != None
145
+ match current
146
+ Some(node) =>
147
+ result.items.add(cb(node.value.key, node.value.val))
148
+ current = node.next
143
- # one method:
149
+ None =>
144
- #
150
+ break
145
- # 1. A generic method's BODY is never substituted for its OWN extra
146
- # generics — `specializeFn` only substitutes PARAM/RETURN types,
147
- # cloning the body as-is. An explicit `Map[X, Y](items: List[Pair[X,
148
- # Y]](...))` construction written directly in `map`'s body therefore
149
- # keeps the LITERAL letters "X"/"Y" forever, which `resolveFieldType`
150
- # then treats as if they were real concrete type names — silently
151
- # manufacturing bogus `List$X`/`Pair$X`-style "specializations" that
152
- # corrupt unrelated `List`/`Node` template compilation elsewhere in the
153
- # SAME program (confirmed: broke `List.add`/`sort`/`find`/etc, none of
154
- # which `map` even touches). Worked around by building the result via
155
- # `self.items.map(...)` (whose own extra-generic resolution IS
156
- # per-call-site, not body-substitution-dependent) plus a BARE `Map(items:
157
- # ...)` construction relying on `current_return_type`, matching
158
- # `keys`/`values` above — this part alone got further than before.
159
- # 2. Once past that, `self.items.map(|p| cb(p.key, p.val))` still fails:
160
- # `plum-checker/src/lib.rs`'s `inferExpr` never propagates a method
161
- # CALL's own declared param types down as an "expected type" hint when
162
- # inferring a closure-literal ARGUMENT — it only guesses a closure
163
- # param's type from field-ACCESS patterns within the closure's own body
164
- # (`resolveClosureParamFromFieldUsage`), which can't know `p`'s class is
165
- # `Pair` at all here. Fixing this needs threading an expected `PlumType`
166
- # through `inferExpr`'s closure-literal handling at (at least) 8 call
167
- # sites in `lib.rs` — a materially bigger, riskier change than
168
- # warranted for this one method; not attempted.
169
- fun map(self) =
151
+ return result
170
- todo
171
152
 
172
153
  fun makeStrIntMapForTest() -> Map[Str, Int] =
173
154
  return Map[Str, Int](items: List[Pair[Str, Int]](head: None, tail: None, size: 0))
@@ -218,3 +199,12 @@ test "each calls the callback with every key and value"
218
199
  seen.clear()
219
200
  m.each(|k, v| seen.add(v))
220
201
  expect seen.join(",") == "1,2"
202
+
203
+ test "map transforms every key/value pair into a new map, changing the value type"
204
+ m := makeStrIntMapForTest()
205
+ m.set("a", 1)
206
+ m.set("b", 2)
207
+ stringified := m.map(|k, v| Pair(key: k, val: v.toStr()))
208
+ expect stringified.get("a").unwrap() == "1"
209
+ expect stringified.get("b").unwrap() == "2"
210
+ expect stringified.length() == 2
plum-checker/src/monomorphize.rs CHANGED
@@ -213,7 +213,143 @@ pub fn specializeFn(f: &ast::Fn, subst: &Substitution, mangled_name: &str, new_t
213
213
  default: p.default.clone(),
214
214
  }).collect(),
215
215
  returns: f.returns.as_ref().map(|r| substituteType(r, subst)),
216
+ body: {
216
- body: f.body.clone(),
217
+ let mut body = f.body.clone();
218
+ match &mut body {
219
+ ast::FnBody::Block(b) => substituteTypesInBlock(b, subst),
220
+ ast::FnBody::Expr(e) => substituteTypesInExpr(e, subst),
221
+ ast::FnBody::Extern => {}
222
+ }
223
+ body
224
+ },
225
+ }
226
+ }
227
+
228
+ /// Rewrites every explicit generic-type-argument annotation reachable inside
229
+ /// `block` (currently only `ClassCall.generics`, e.g. the `[X, Y]` in
230
+ /// `Map[X, Y](...)`) through `subst` — the SAME substitution `specializeFn`
231
+ /// already applies to a method's own params/return, just extended to its
232
+ /// BODY. Without this, a method with its own extra generic param(s) (beyond
233
+ /// its receiver's) that writes an explicit bracketed construction using one
234
+ /// of those letters keeps the literal, un-substituted letter ("X") after
235
+ /// specialization: `resolveClassInstantiation` then treats "X" as a real
236
+ /// (nonexistent) class name, silently minting bogus `List$X`/`Pair$X`
237
+ /// "specializations" that corrupt unrelated template compilation. See the
238
+ /// `plum_map_map_gaps_2026_09` memory (gap 1) for the discovery writeup.
239
+ fn substituteTypesInBlock(block: &mut ast::Block, subst: &Substitution) {
240
+ for stmt in &mut block.stmts {
241
+ substituteTypesInStmt(stmt, subst);
242
+ }
243
+ }
244
+
245
+ fn substituteTypesInStmt(stmt: &mut ast::Stmt, subst: &Substitution) {
246
+ match stmt {
247
+ ast::Stmt::Assign(a) => {
248
+ for v in &mut a.values {
249
+ substituteTypesInExpr(v, subst);
250
+ }
251
+ for t in &mut a.targets {
252
+ if let ast::AssignTarget::Field(obj, _) = t {
253
+ substituteTypesInExpr(obj, subst);
254
+ }
255
+ }
256
+ }
257
+ ast::Stmt::Break | ast::Stmt::Continue | ast::Stmt::Todo => {}
258
+ ast::Stmt::Assert(e) | ast::Stmt::Expect(e) => substituteTypesInExpr(e, subst),
259
+ ast::Stmt::For(f) => {
260
+ substituteTypesInExpr(&mut f.iter, subst);
261
+ substituteTypesInBlock(&mut f.body, subst);
262
+ }
263
+ ast::Stmt::While(w) => {
264
+ substituteTypesInExpr(&mut w.condition, subst);
265
+ substituteTypesInBlock(&mut w.body, subst);
266
+ }
267
+ ast::Stmt::If(i) => {
268
+ substituteTypesInExpr(&mut i.condition, subst);
269
+ substituteTypesInBlock(&mut i.body, subst);
270
+ for ei in &mut i.else_ifs {
271
+ substituteTypesInExpr(&mut ei.condition, subst);
272
+ substituteTypesInBlock(&mut ei.body, subst);
273
+ }
274
+ if let Some(e) = &mut i.else_ {
275
+ substituteTypesInBlock(e, subst);
276
+ }
277
+ }
278
+ ast::Stmt::Match(m) => {
279
+ for s in &mut m.subjects {
280
+ substituteTypesInExpr(s, subst);
281
+ }
282
+ for c in &mut m.cases {
283
+ substituteTypesInBlock(&mut c.body, subst);
284
+ }
285
+ }
286
+ ast::Stmt::Return(Some(e)) => substituteTypesInExpr(e, subst),
287
+ ast::Stmt::Return(None) => {}
288
+ ast::Stmt::Expr(e) => substituteTypesInExpr(e, subst),
289
+ }
290
+ }
291
+
292
+ fn substituteTypesInArg(arg: &mut ast::Arg, subst: &Substitution) {
293
+ match arg {
294
+ ast::Arg::Positional(e) => substituteTypesInExpr(e, subst),
295
+ ast::Arg::Keyword { value, .. } => substituteTypesInExpr(value, subst),
296
+ ast::Arg::Pair { value, .. } => substituteTypesInExpr(value, subst),
297
+ }
298
+ }
299
+
300
+ fn substituteTypesInExpr(expr: &mut ast::Expr, subst: &Substitution) {
301
+ match expr {
302
+ ast::Expr::Binary(b) => {
303
+ substituteTypesInExpr(&mut b.left, subst);
304
+ substituteTypesInExpr(&mut b.right, subst);
305
+ }
306
+ ast::Expr::Unary(u) => substituteTypesInExpr(&mut u.operand, subst),
307
+ ast::Expr::Bool(b) => {
308
+ substituteTypesInExpr(&mut b.left, subst);
309
+ substituteTypesInExpr(&mut b.right, subst);
310
+ }
311
+ ast::Expr::Not(inner) => substituteTypesInExpr(inner, subst),
312
+ ast::Expr::Compare(c) => {
313
+ substituteTypesInExpr(&mut c.left, subst);
314
+ substituteTypesInExpr(&mut c.right, subst);
315
+ }
316
+ ast::Expr::Ternary(t) => {
317
+ substituteTypesInExpr(&mut t.condition, subst);
318
+ substituteTypesInExpr(&mut t.then, subst);
319
+ substituteTypesInExpr(&mut t.else_, subst);
320
+ }
321
+ ast::Expr::FnCall(call) => {
322
+ for arg in &mut call.args {
323
+ substituteTypesInArg(arg, subst);
324
+ }
325
+ }
326
+ ast::Expr::ClassCall(call) => {
327
+ for g in &mut call.generics {
328
+ *g = substituteType(g, subst);
329
+ }
330
+ for fa in &mut call.fields {
331
+ substituteTypesInExpr(&mut fa.value, subst);
332
+ }
333
+ }
334
+ ast::Expr::Attribute(attr) => {
335
+ substituteTypesInExpr(&mut attr.object, subst);
336
+ if let ast::AttrKind::Method(call) = &mut attr.attr {
337
+ for arg in &mut call.args {
338
+ substituteTypesInArg(arg, subst);
339
+ }
340
+ }
341
+ }
342
+ ast::Expr::Paren(inner) => substituteTypesInExpr(inner, subst),
343
+ ast::Expr::String(s) => {
344
+ for part in &mut s.parts {
345
+ if let ast::StringPart::Interp(e) = part {
346
+ substituteTypesInExpr(e, subst);
347
+ }
348
+ }
349
+ }
350
+ ast::Expr::Closure(cl) => substituteTypesInBlock(&mut cl.body, subst),
351
+ ast::Expr::Int(_) | ast::Expr::Float(_)
352
+ | ast::Expr::Self_ | ast::Expr::Var(_) | ast::Expr::TypeName(_) => {}
217
353
  }
218
354
  }
219
355