plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
cde99bd
— Peter John
2026-09-03T14:06:30+05:30
fix(plum-checker): stop the self-nesting generic-method stack overflow
- libs/std/list.plum +24 -19
- 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
|
-
# `
|
|
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.
|
|
373
|
+
# (`List`) nested inside itself as a method's own return type. This used to
|
|
375
|
-
#
|
|
374
|
+
# be an unconditional stack-overflow landmine for the WHOLE COMPILER (see
|
|
376
|
-
# `plum-checker/src/monomorphize.rs`
|
|
375
|
+
# `plum-checker/src/monomorphize.rs`'s `isSelfNested`/`methodMentionsTemplate`
|
|
377
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
381
|
+
# specialized `add` method on the resulting `List[List[T]]` (e.g.
|
|
383
|
-
#
|
|
382
|
+
# `List$List$Int`) gets registered with a WRONG/confused parameter type
|
|
384
|
-
# merely `import std/list` stack-overflow the compiler, since nothing
|
|
385
|
-
#
|
|
383
|
+
# (`expected List$Int, found Int` and, inconsistently, `expected List$Int,
|
|
386
|
-
#
|
|
384
|
+
# found List$List$Int` for the very same key, across different reported
|
|
387
|
-
#
|
|
385
|
+
# call sites) — root cause not yet found; suspected signature-registration
|
|
388
|
-
#
|
|
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
|
|
389
|
+
fun chunk(self) =
|
|
390
390
|
todo
|
|
391
391
|
|
|
392
|
-
fun
|
|
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,
|
|
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,
|
|
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],
|
|
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
|
-
|
|
2253
|
+
if let Some(methods) = m.methods_generic_on.get(base.name.as_str()).cloned() {
|
|
2183
|
-
|
|
2254
|
+
let owner_params = classGenericParams(base);
|
|
2184
|
-
|
|
2255
|
+
for method in methods {
|
|
2185
|
-
|
|
2256
|
+
// A method with generic param(s) of its OWN (e.g. `map`'s
|
|
2186
|
-
|
|
2257
|
+
// `U`, beyond `List[T]`'s own `T`) can't be produced HERE —
|
|
2187
|
-
|
|
2258
|
+
// `U` is only known at a particular CALL SITE, not at class-
|
|
2188
|
-
|
|
2259
|
+
// specialization time. `resolveMethodOwnGenerics` queues a
|
|
2189
|
-
|
|
2260
|
+
// `PendingSpecialization::Fn` for each concrete `U` it
|
|
2190
|
-
|
|
2261
|
+
// actually sees used, instead.
|
|
2191
|
-
|
|
2262
|
+
if !methodOwnGenericParams(method, &owner_params).is_empty() {
|
|
2192
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2288
|
+
}).collect();
|
|
2215
|
-
|
|
2289
|
+
let ret = specialized_method.returns.as_ref()
|
|
2216
|
-
|
|
2290
|
+
.map(crate::plumTypeFromAst)
|
|
2217
|
-
|
|
2291
|
+
.unwrap_or(PlumType::TUnit);
|
|
2218
|
-
|
|
2292
|
+
m.methods.insert((mangled.clone(), specialized_method.name.clone()), PlumType::TFun(param_types, Box::new(ret)));
|
|
2219
|
-
|
|
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
|
-
|
|
2338
|
+
if let Some(methods) = m.methods_generic_on_enum.get(base.name.as_str()).cloned() {
|
|
2260
|
-
|
|
2339
|
+
let owner_params = enumGenericParams(base);
|
|
2261
|
-
|
|
2340
|
+
for method in methods {
|
|
2262
|
-
|
|
2341
|
+
// See the matching comment in the `Class` arm above.
|
|
2263
|
-
|
|
2342
|
+
if !methodOwnGenericParams(method, &owner_params).is_empty() {
|
|
2264
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2360
|
+
}).collect();
|
|
2279
|
-
|
|
2361
|
+
let ret = specialized_method.returns.as_ref()
|
|
2280
|
-
|
|
2362
|
+
.map(crate::plumTypeFromAst)
|
|
2281
|
-
|
|
2363
|
+
.unwrap_or(PlumType::TUnit);
|
|
2282
|
-
|
|
2364
|
+
m.methods.insert((mangled.clone(), specialized_method.name.clone()), PlumType::TFun(param_types, Box::new(ret)));
|
|
2283
|
-
|
|
2365
|
+
m.produced.push(ast::Item::Fn(specialized_method));
|
|
2366
|
+
}
|
|
2284
2367
|
}
|
|
2285
2368
|
}
|
|
2286
2369
|
}
|