plum

#treesitter#compiler#wasm

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

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


cde99bdPeter John 2026-09-03T14:06:30+05:30
fix(plum-checker): stop the self-nesting generic-method stack overflow
Files changed (2) hide show
  1. libs/std/list.plum +24 -19
  2. plum-checker/src/monomorphize.rs +148 -65
libs/std/list.plum CHANGED
@@ -369,29 +369,34 @@ type List[T: Stringable](Stringable) =
369
369
  i = i - 1
370
370
  return result
371
371
 
372
- # `partition`/`chunk`/`groupBy` would each need to return `List[List[T]]`
372
+ # `chunk`/`partition` both return `List[List[T]]` — the SAME generic class
373
- # (or `Map[K, List[T]]`) — the SAME generic class (`List`) nested inside
374
- # itself as a method's own return type. `methods_generic_on`'s eager
373
+ # (`List`) nested inside itself as a method's own return type. This used to
375
- # registration (`registerClassMethodSignatures` in
374
+ # be an unconditional stack-overflow landmine for the WHOLE COMPILER (see
376
- # `plum-checker/src/monomorphize.rs`, which resolves and registers EVERY
375
+ # `plum-checker/src/monomorphize.rs`'s `isSelfNested`/`methodMentionsTemplate`
377
- # method's signature immediately whenever its owning class is specialized
376
+ # doc comments for the full fix) that danger is now FIXED (2026-09-02),
377
+ # verified: a program merely `import`ing `std/list` with these methods
378
+ # DEFINED no longer hangs/crashes regardless of whether anything calls
379
+ # them. `chunk`/`partition` THEMSELVES are still not implemented, though —
378
- # at ANY concrete type, not lazily on first actual call) makes that an
380
+ # actually CALLING them still hits a second, more contained bug: the
379
- # infinite regress: specializing `List[X]` resolves `chunk`'s return type
380
- # `List[List[X]]`, which is a NEW, different specialization of `List` —
381
- # eagerly specializing IT immediately re-triggers the exact same
382
- # registration for ITS OWN `chunk`, needing `List[List[List[X]]]`, forever
381
+ # specialized `add` method on the resulting `List[List[T]]` (e.g.
383
- # (confirmed: adding these methods made even unrelated programs that
382
+ # `List$List$Int`) gets registered with a WRONG/confused parameter type
384
- # merely `import std/list` stack-overflow the compiler, since nothing
385
- # needs to actually CALL `chunk` to trigger it — just having it exist on
383
+ # (`expected List$Int, found Int` and, inconsistently, `expected List$Int,
386
- # `List[T]` is enough). Fixing this for real needs lazy (call-site-driven,
384
+ # found List$List$Int` for the very same key, across different reported
387
- # not eager) method specialization, a substantially bigger change than
385
+ # call sites) root cause not yet found; suspected signature-registration
388
- # this method itself. Not implemented.
386
+ # aliasing between a self-nested specialization and its own inner
387
+ # specialization, possibly compounded by both keeping the SAME bare method
388
+ # name (`specializeFn` never mangles an ordinary method's own name).
389
- fun partition(self) =
389
+ fun chunk(self) =
390
390
  todo
391
391
 
392
- fun chunk(self) =
392
+ fun partition(self) =
393
393
  todo
394
394
 
395
+ # Grouping by an arbitrary key type would need its own generic param
396
+ # (`groupBy<K>(keyFn: fn(T) -> K) -> Map[K, List[T]]`) resolved from a
397
+ # closure's return type — the same method-level-generics gap documented on
398
+ # `List.map` above, PLUS `Map[K, List[T]]` would make `list.plum` depend on
399
+ # `map.plum`, which already depends on `list.plum`. Not implemented.
395
400
  fun groupBy(self) =
396
401
  todo
397
402
 
plum-checker/src/monomorphize.rs CHANGED
@@ -1222,7 +1222,7 @@ impl<'a> Monomorphizer<'a> {
1222
1222
  fn registerClassMethodSignatures(&mut self, class: &'a ast::Class, mangled: &str, bindings: &BTreeMap<String, PlumType>) {
1223
1223
  let Some(methods) = self.methods_generic_on.get(class.name.as_str()).cloned() else { return };
1224
1224
  let owner_params = classGenericParams(class);
1225
- self.registerMethodSignatures(methods, &owner_params, mangled, bindings);
1225
+ self.registerMethodSignatures(&class.name, mangled, methods, &owner_params, bindings);
1226
1226
  }
1227
1227
 
1228
1228
  /// The enum counterpart to `registerClassMethodSignatures` — see its doc
@@ -1240,7 +1240,7 @@ impl<'a> Monomorphizer<'a> {
1240
1240
  fn registerEnumMethodSignatures(&mut self, e: &'a ast::Enum, mangled: &str, bindings: &BTreeMap<String, PlumType>) {
1241
1241
  let Some(methods) = self.methods_generic_on_enum.get(e.name.as_str()).cloned() else { return };
1242
1242
  let owner_params = enumGenericParams(e);
1243
- self.registerMethodSignatures(methods, &owner_params, mangled, bindings);
1243
+ self.registerMethodSignatures(&e.name, mangled, methods, &owner_params, bindings);
1244
1244
  }
1245
1245
 
1246
1246
  /// Shared body of `registerClassMethodSignatures`/`registerEnumMethodSignatures`
@@ -1249,8 +1249,19 @@ impl<'a> Monomorphizer<'a> {
1249
1249
  /// of its own — see the skip below) method in `methods`, for the
1250
1250
  /// specialization named `mangled` under `bindings`, without producing the
1251
1251
  /// actual `ast::Fn` bodies (that still only happens once the worklist
1252
- /// entry for this specialization is popped).
1252
+ /// entry for this specialization is popped). If this specialization is
1253
+ /// itself already nested (`isSelfNested`), skips (only) a method whose
1254
+ /// return type would construct yet another nested specialization
1255
+ /// (`methodMentionsTemplate`) — the self-nesting landmine. Deliberately
1256
+ /// NOT a blanket skip for every method whenever `mangled` is
1257
+ /// self-nested: `List$List$Int`'s own `add`/`get`/etc are perfectly
1258
+ /// finite regardless (their return types don't mention `List` at all)
1259
+ /// and are needed for `chunk`'s own body (`result.add(piece)`) to even
1260
+ /// compile — only `chunk`/`partition` THEMSELVES, called again on an
1261
+ /// already-nested `List$List$Int`, would recurse into a third level and
1262
+ /// must be skipped.
1253
- fn registerMethodSignatures(&mut self, methods: Vec<&'a ast::Fn>, owner_params: &[String], mangled: &str, bindings: &BTreeMap<String, PlumType>) {
1263
+ fn registerMethodSignatures(&mut self, template_name: &str, mangled: &str, methods: Vec<&'a ast::Fn>, owner_params: &[String], bindings: &BTreeMap<String, PlumType>) {
1264
+ let already_nested = self.isSelfNested(template_name, bindings);
1254
1265
  for method in methods {
1255
1266
  // See the matching comment in the `PendingSpecialization::Class`/
1256
1267
  // `PendingSpecialization::Enum` worklist arms — a method with its
@@ -1259,6 +1270,9 @@ impl<'a> Monomorphizer<'a> {
1259
1270
  if !methodOwnGenericParams(method, owner_params).is_empty() {
1260
1271
  continue;
1261
1272
  }
1273
+ if already_nested && Self::methodMentionsTemplate(method, template_name) {
1274
+ continue;
1275
+ }
1262
1276
  let key = (mangled.to_string(), method.name.clone());
1263
1277
  if self.methods.contains_key(&key) {
1264
1278
  continue;
@@ -1304,6 +1318,50 @@ impl<'a> Monomorphizer<'a> {
1304
1318
  }
1305
1319
  }
1306
1320
 
1321
+ /// True if the specialization about to be processed is ITSELF already a
1322
+ /// nested instance of `template_name` — one of `bindings`'s VALUES is
1323
+ /// either `template_name` itself or a recorded specialization OF it
1324
+ /// (via `class_specialization_info`), e.g. `List$Int`'s `T` bound to
1325
+ /// `Int` is NOT self-nested, but `List$List$Int`'s `T` bound to
1326
+ /// `List$Int` IS. Guards the self-nesting landmine (a method returning
1327
+ /// its OWN owning class/enum nested in itself, e.g. `List[T].chunk() ->
1328
+ /// List[List[T]]`): resolving `chunk`'s return type for a specialization
1329
+ /// that's ALREADY nested would create yet another, deeper nested
1330
+ /// specialization, forever — and because this eager registration
1331
+ /// happens once per WORKLIST entry (processed iteratively, not
1332
+ /// recursively), a plain call-stack-scoped recursion guard tried first
1333
+ /// was NOT enough to stop it (confirmed: hung 20+s with only that).
1334
+ /// Combined with `methodMentionsTemplate` (only reachable, and only
1335
+ /// meaningful, when this is true) — a specialization's OWN `add`/`get`/
1336
+ /// etc are perfectly finite regardless of nesting and must NOT be
1337
+ /// skipped, only a method whose return type would construct yet ANOTHER
1338
+ /// nested specialization.
1339
+ fn isSelfNested(&self, template_name: &str, bindings: &BTreeMap<String, PlumType>) -> bool {
1340
+ bindings.values().any(|t| match t {
1341
+ PlumType::TNamed(n) => {
1342
+ n == template_name
1343
+ || self.class_specialization_info.get(n.as_str())
1344
+ .map(|(base, _)| base == template_name)
1345
+ .unwrap_or(false)
1346
+ }
1347
+ _ => false,
1348
+ })
1349
+ }
1350
+
1351
+ /// True if `method`'s OWN declared return type mentions `template_name`
1352
+ /// anywhere (at any nesting depth, INCLUDING the top level — unlike a
1353
+ /// naive structural check, depth doesn't matter here: this is only ever
1354
+ /// consulted once `isSelfNested` has already confirmed the CURRENT
1355
+ /// specialization is itself nested, at which point even a top-level
1356
+ /// `List[...]` return needs a new, deeper nested specialization). See
1357
+ /// `isSelfNested`'s doc comment for the full picture.
1358
+ fn methodMentionsTemplate(method: &ast::Fn, template_name: &str) -> bool {
1359
+ fn mentions(ty: &ast::Type, template_name: &str) -> bool {
1360
+ ty.name == template_name || ty.generics.iter().any(|g| mentions(g, template_name))
1361
+ }
1362
+ method.returns.as_ref().map(|ret| mentions(ret, template_name)).unwrap_or(false)
1363
+ }
1364
+
1307
1365
  /// Fills `out` with a binding for every bare generic-letter type occurring
1308
1366
  /// in `declared` (possibly nested inside a container type, e.g. `T` inside
1309
1367
  /// `List[T]`), given `actual` — the ACTUAL concrete `PlumType` an argument
@@ -2179,44 +2237,61 @@ pub fn monomorphizeSource(source: &ast::Source) -> Result<ast::Source, String> {
2179
2237
  spec_class.fields.iter().map(|f| (f.name.clone(), crate::plumTypeFromAst(&f.ty))).collect(),
2180
2238
  );
2181
2239
  m.produced.push(ast::Item::Class(spec_class));
2240
+ // Same self-nesting landmine as `registerMethodSignatures` —
2241
+ // if THIS specialization is itself already nested
2242
+ // (`isSelfNested`), skips (only) a method whose return type
2243
+ // would construct yet another nested specialization
2244
+ // (`methodMentionsTemplate`; see `isSelfNested`'s doc
2245
+ // comment). Without this, `List.chunk() -> List[List[T]]`
2246
+ // would recurse into producing an unbounded chain of
2247
+ // ever-more-nested `List` specializations via THIS SAME
2248
+ // loop — and since this loop runs once per separate
2249
+ // WORKLIST pop rather than recursively, a guard scoped to a
2250
+ // single call stack isn't enough on its own.
2251
+ {
2252
+ let already_nested = m.isSelfNested(&base.name, &subst.0);
2182
- if let Some(methods) = m.methods_generic_on.get(base.name.as_str()).cloned() {
2253
+ if let Some(methods) = m.methods_generic_on.get(base.name.as_str()).cloned() {
2183
- let owner_params = classGenericParams(base);
2254
+ let owner_params = classGenericParams(base);
2184
- for method in methods {
2255
+ for method in methods {
2185
- // A method with generic param(s) of its OWN (e.g. `map`'s
2256
+ // A method with generic param(s) of its OWN (e.g. `map`'s
2186
- // `U`, beyond `List[T]`'s own `T`) can't be produced HERE —
2257
+ // `U`, beyond `List[T]`'s own `T`) can't be produced HERE —
2187
- // `U` is only known at a particular CALL SITE, not at class-
2258
+ // `U` is only known at a particular CALL SITE, not at class-
2188
- // specialization time. `resolveMethodOwnGenerics` queues a
2259
+ // specialization time. `resolveMethodOwnGenerics` queues a
2189
- // `PendingSpecialization::Fn` for each concrete `U` it
2260
+ // `PendingSpecialization::Fn` for each concrete `U` it
2190
- // actually sees used, instead.
2261
+ // actually sees used, instead.
2191
- if !methodOwnGenericParams(method, &owner_params).is_empty() {
2262
+ if !methodOwnGenericParams(method, &owner_params).is_empty() {
2192
- continue;
2263
+ continue;
2193
- }
2264
+ }
2194
- let mut specialized_method = specializeFn(method, &subst, &method.name, Some(mangled.clone()));
2195
- // Params (not returns — see `resolveFnParamTypes`'s doc
2196
- // comment) must run before `rewriteFnBody`/registration
2197
- // below: both read the signature's types directly off
2198
- // this AST, and the real checker later rebuilds its own
2199
- // tables from this exact (post-monomorphize) AST too.
2200
- m.resolveFnParamTypes(&mut specialized_method);
2201
- m.rewriteFnBody(&mut specialized_method, true)?;
2202
- m.resolveFnReturnType(&mut specialized_method);
2203
- // Register the specialized method's signature under its
2204
- // (mangled receiver, method name) key so any later body that
2205
- // dispatches to it can resolve its concrete return type.
2206
- let param_types: Vec<PlumType> = specialized_method.params.iter().map(|p| match &p.ty {
2265
+ if already_nested && Monomorphizer::methodMentionsTemplate(method, &base.name) {
2207
- ast::ParamType::Type(t) => crate::plumTypeFromAst(t),
2208
- ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(crate::plumTypeFromAst(t))),
2209
- ast::ParamType::Fn(params, ret) => {
2266
+ continue;
2210
- let param_types = params.iter().map(crate::plumTypeFromAst).collect();
2211
- let ret_ty = ret.as_ref().map(|r| crate::plumTypeFromAst(r)).unwrap_or(PlumType::TUnit);
2212
- PlumType::TFun(param_types, Box::new(ret_ty))
2213
2267
  }
2268
+ let mut specialized_method = specializeFn(method, &subst, &method.name, Some(mangled.clone()));
2269
+ // Params (not returns — see `resolveFnParamTypes`'s doc
2270
+ // comment) must run before `rewriteFnBody`/registration
2271
+ // below: both read the signature's types directly off
2272
+ // this AST, and the real checker later rebuilds its own
2273
+ // tables from this exact (post-monomorphize) AST too.
2274
+ m.resolveFnParamTypes(&mut specialized_method);
2275
+ m.rewriteFnBody(&mut specialized_method, true)?;
2276
+ m.resolveFnReturnType(&mut specialized_method);
2277
+ // Register the specialized method's signature under its
2278
+ // (mangled receiver, method name) key so any later body that
2279
+ // dispatches to it can resolve its concrete return type.
2280
+ let param_types: Vec<PlumType> = specialized_method.params.iter().map(|p| match &p.ty {
2281
+ ast::ParamType::Type(t) => crate::plumTypeFromAst(t),
2282
+ ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(crate::plumTypeFromAst(t))),
2283
+ ast::ParamType::Fn(params, ret) => {
2284
+ let param_types = params.iter().map(crate::plumTypeFromAst).collect();
2285
+ let ret_ty = ret.as_ref().map(|r| crate::plumTypeFromAst(r)).unwrap_or(PlumType::TUnit);
2286
+ PlumType::TFun(param_types, Box::new(ret_ty))
2287
+ }
2214
- }).collect();
2288
+ }).collect();
2215
- let ret = specialized_method.returns.as_ref()
2289
+ let ret = specialized_method.returns.as_ref()
2216
- .map(crate::plumTypeFromAst)
2290
+ .map(crate::plumTypeFromAst)
2217
- .unwrap_or(PlumType::TUnit);
2291
+ .unwrap_or(PlumType::TUnit);
2218
- m.methods.insert((mangled.clone(), specialized_method.name.clone()), PlumType::TFun(param_types, Box::new(ret)));
2292
+ m.methods.insert((mangled.clone(), specialized_method.name.clone()), PlumType::TFun(param_types, Box::new(ret)));
2219
- m.produced.push(ast::Item::Fn(specialized_method));
2293
+ m.produced.push(ast::Item::Fn(specialized_method));
2294
+ }
2220
2295
  }
2221
2296
  }
2222
2297
  }
@@ -2256,31 +2331,39 @@ pub fn monomorphizeSource(source: &ast::Source) -> Result<ast::Source, String> {
2256
2331
  }
2257
2332
  }
2258
2333
  m.produced.push(ast::Item::Enum(spec_enum));
2334
+ // Guarded against the same self-nesting landmine as the
2335
+ // `Class` arm above — see `isSelfNested`'s doc comment.
2336
+ {
2337
+ let already_nested = m.isSelfNested(&base.name, &subst.0);
2259
- if let Some(methods) = m.methods_generic_on_enum.get(base.name.as_str()).cloned() {
2338
+ if let Some(methods) = m.methods_generic_on_enum.get(base.name.as_str()).cloned() {
2260
- let owner_params = enumGenericParams(base);
2339
+ let owner_params = enumGenericParams(base);
2261
- for method in methods {
2340
+ for method in methods {
2262
- // See the matching comment in the `Class` arm above.
2341
+ // See the matching comment in the `Class` arm above.
2263
- if !methodOwnGenericParams(method, &owner_params).is_empty() {
2342
+ if !methodOwnGenericParams(method, &owner_params).is_empty() {
2264
- continue;
2343
+ continue;
2265
- }
2344
+ }
2266
- let mut specialized_method = specializeFn(method, &subst, &method.name, Some(mangled.clone()));
2267
- m.resolveFnParamTypes(&mut specialized_method);
2268
- m.rewriteFnBody(&mut specialized_method, true)?;
2269
- m.resolveFnReturnType(&mut specialized_method);
2270
- let param_types: Vec<PlumType> = specialized_method.params.iter().map(|p| match &p.ty {
2345
+ if already_nested && Monomorphizer::methodMentionsTemplate(method, &base.name) {
2271
- ast::ParamType::Type(t) => crate::plumTypeFromAst(t),
2272
- ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(crate::plumTypeFromAst(t))),
2273
- ast::ParamType::Fn(params, ret) => {
2346
+ continue;
2274
- let param_types = params.iter().map(crate::plumTypeFromAst).collect();
2275
- let ret_ty = ret.as_ref().map(|r| crate::plumTypeFromAst(r)).unwrap_or(PlumType::TUnit);
2276
- PlumType::TFun(param_types, Box::new(ret_ty))
2277
2347
  }
2348
+ let mut specialized_method = specializeFn(method, &subst, &method.name, Some(mangled.clone()));
2349
+ m.resolveFnParamTypes(&mut specialized_method);
2350
+ m.rewriteFnBody(&mut specialized_method, true)?;
2351
+ m.resolveFnReturnType(&mut specialized_method);
2352
+ let param_types: Vec<PlumType> = specialized_method.params.iter().map(|p| match &p.ty {
2353
+ ast::ParamType::Type(t) => crate::plumTypeFromAst(t),
2354
+ ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(crate::plumTypeFromAst(t))),
2355
+ ast::ParamType::Fn(params, ret) => {
2356
+ let param_types = params.iter().map(crate::plumTypeFromAst).collect();
2357
+ let ret_ty = ret.as_ref().map(|r| crate::plumTypeFromAst(r)).unwrap_or(PlumType::TUnit);
2358
+ PlumType::TFun(param_types, Box::new(ret_ty))
2359
+ }
2278
- }).collect();
2360
+ }).collect();
2279
- let ret = specialized_method.returns.as_ref()
2361
+ let ret = specialized_method.returns.as_ref()
2280
- .map(crate::plumTypeFromAst)
2362
+ .map(crate::plumTypeFromAst)
2281
- .unwrap_or(PlumType::TUnit);
2363
+ .unwrap_or(PlumType::TUnit);
2282
- m.methods.insert((mangled.clone(), specialized_method.name.clone()), PlumType::TFun(param_types, Box::new(ret)));
2364
+ m.methods.insert((mangled.clone(), specialized_method.name.clone()), PlumType::TFun(param_types, Box::new(ret)));
2283
- m.produced.push(ast::Item::Fn(specialized_method));
2365
+ m.produced.push(ast::Item::Fn(specialized_method));
2366
+ }
2284
2367
  }
2285
2368
  }
2286
2369
  }