plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
accb2b1
— Peter 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
- README.md +1 -1
- libs/std/map.plum +20 -30
- 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
|
|
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
|
-
|
|
141
|
+
fun map(self, cb: fn(K, V) -> Pair[X, Y]) -> Map[X, Y] =
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|