plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
5a9e763
— Peter John
2026-09-03T14:25:26+05:30
feat(libs/std): ship List.chunk/partition; add List.toStr; root-cause the last self-nesting bug
- README.md +2 -1
- libs/std/list.plum +82 -17
README.md
CHANGED
|
@@ -390,4 +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. `Map`'s `map` method (needing
|
|
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
|
|
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/list.plum
CHANGED
|
@@ -373,24 +373,52 @@ type List[T: Stringable](Stringable) =
|
|
|
373
373
|
# (`List`) nested inside itself as a method's own return type. This used to
|
|
374
374
|
# be an unconditional stack-overflow landmine for the WHOLE COMPILER (see
|
|
375
375
|
# `plum-checker/src/monomorphize.rs`'s `isSelfNested`/`methodMentionsTemplate`
|
|
376
|
-
# doc comments for the full fix) — that danger is now FIXED (2026-09-02)
|
|
376
|
+
# doc comments for the full fix) — that danger is now FIXED (2026-09-02).
|
|
377
|
+
#
|
|
378
|
+
# A second, more contained bug surfaced once past that: two SEPARATE bare
|
|
377
|
-
#
|
|
379
|
+
# `List(head: None, tail: None, size: 0)` constructions in the SAME method
|
|
378
|
-
# DEFINED no longer hangs/crashes regardless of whether anything calls
|
|
379
|
-
# them. `chunk`/`partition` THEMSELVES are still not implemented, though —
|
|
380
|
-
# actually CALLING them still hits a second, more contained bug: the
|
|
381
|
-
#
|
|
380
|
+
# body — one for the OUTER `result: List[List[T]]` (matching this
|
|
382
|
-
#
|
|
381
|
+
# method's own declared return type) and one for an INNER per-chunk
|
|
383
|
-
#
|
|
382
|
+
# `piece: List[T]` — are ambiguous to `resolveClassInstantiation`'s
|
|
383
|
+
# `current_return_type` fallback: since `List[T]` and `List[List[T]]` both
|
|
384
|
+
# have exactly ONE generic argument, the fallback (which only ever
|
|
385
|
+
# compares ARITY, not identity) can't tell "this bare construction IS the
|
|
386
|
+
# function's own return value" apart from "this is some OTHER, unrelated
|
|
384
|
-
#
|
|
387
|
+
# nested construction of the same template that happens to need the same
|
|
385
|
-
# call sites) — root cause not yet found; suspected signature-registration
|
|
386
|
-
# aliasing between a self-nested specialization and its own inner
|
|
387
|
-
#
|
|
388
|
+
# arity" — so `piece` was silently resolved to the SAME type as `result`
|
|
389
|
+
# (`List$List$Int` instead of `List$Int`), corrupting its own `add` calls.
|
|
390
|
+
# Worked around by building `piece` via `self.sublist(...)` instead of a
|
|
388
|
-
#
|
|
391
|
+
# second bare construction — `sublist` is an ordinary method already
|
|
392
|
+
# unambiguously resolved through the RECEIVER's own (already-correct)
|
|
393
|
+
# binding, no `current_return_type` guessing involved at all.
|
|
394
|
+
fun chunk(self, size: Int) -> List[List[T]] =
|
|
395
|
+
result := List(head: None, tail: None, size: 0)
|
|
389
|
-
|
|
396
|
+
if size <= 0
|
|
397
|
+
return result
|
|
390
|
-
|
|
398
|
+
i := 0
|
|
399
|
+
n := self.length()
|
|
400
|
+
while i < n
|
|
401
|
+
result.add(self.sublist(i, i + size))
|
|
402
|
+
i = i + size
|
|
403
|
+
return result
|
|
391
404
|
|
|
405
|
+
# Splits into two lists: elements `predicate` holds for, then the rest —
|
|
406
|
+
# in that order. Returns them as a 2-element `List[List[T]]` (`[matched,
|
|
407
|
+
# unmatched]`) since this language has no tuple type. Deliberately built
|
|
408
|
+
# from `self.retain(predicate)`/`self.reject(predicate)` rather than two
|
|
409
|
+
# local `matched`/`unmatched := List(head: None, ...)` bare constructions
|
|
410
|
+
# — see `chunk`'s doc comment above for exactly why that would be
|
|
411
|
+
# ambiguous (`matched`/`unmatched` need `List[T]`, but a bare
|
|
412
|
+
# construction here would resolve via `current_return_type` fallback
|
|
413
|
+
# against THIS method's own `List[List[T]]` return instead, same arity
|
|
414
|
+
# coincidence). `retain`/`reject` are each already unambiguous (single
|
|
415
|
+
# bare construction per method body, matching their own single-level
|
|
392
|
-
|
|
416
|
+
# return type).
|
|
417
|
+
fun partition(self, predicate: fn(T) -> Bool) -> List[List[T]] =
|
|
418
|
+
result := List(head: None, tail: None, size: 0)
|
|
419
|
+
result.add(self.retain(predicate))
|
|
420
|
+
result.add(self.reject(predicate))
|
|
393
|
-
|
|
421
|
+
return result
|
|
394
422
|
|
|
395
423
|
# Grouping by an arbitrary key type would need its own generic param
|
|
396
424
|
# (`groupBy<K>(keyFn: fn(T) -> K) -> Map[K, List[T]]`) resolved from a
|
|
@@ -421,6 +449,21 @@ type List[T: Stringable](Stringable) =
|
|
|
421
449
|
break
|
|
422
450
|
res.toStr()
|
|
423
451
|
|
|
452
|
+
# `List[T: Stringable](Stringable)` declared implementing `Stringable`
|
|
453
|
+
# from the start but never actually defined `toStr` — harmless for a
|
|
454
|
+
# PLAIN `List[Int]`/`List[Str]`/etc (nothing needs to call `.toStr()` on
|
|
455
|
+
# the LIST ITSELF, just on its elements, which `join` above already
|
|
456
|
+
# does), but a genuine gap the moment a `List` is used AS another
|
|
457
|
+
# `List`'s own ELEMENT type (`List[List[T]]`) — the OUTER list's eagerly-
|
|
458
|
+
# compiled `join`/`toStr` calls `.toStr()` on each element, and an inner
|
|
459
|
+
# `List[T]` element had no such method to call, failing to compile with
|
|
460
|
+
# "unknown method 'List$T.toStr'" even for a program that never actually
|
|
461
|
+
# calls `.toStr()`/`.join()` on the outer list (per the "eager compile
|
|
462
|
+
# every method for every specialization" architecture noted elsewhere in
|
|
463
|
+
# this codebase's history).
|
|
464
|
+
fun toStr(self) -> Str =
|
|
465
|
+
return "[" + self.join(",") + "]"
|
|
466
|
+
|
|
424
467
|
fun makeList(values: ...T) -> List[T] =
|
|
425
468
|
l := List(head: None, tail: None, size: 0)
|
|
426
469
|
for v := range values
|
|
@@ -503,3 +546,25 @@ test "list sort orders elements correctly"
|
|
|
503
546
|
test "list join renders elements correctly with separator"
|
|
504
547
|
l = makeList(1, 2, 3)
|
|
505
548
|
expect l.join(",") == "1,2,3"
|
|
549
|
+
|
|
550
|
+
test "list toStr formats a plain list and a nested list of lists"
|
|
551
|
+
l := makeList(1, 2, 3)
|
|
552
|
+
expect l.toStr() == "[1,2,3]"
|
|
553
|
+
chunks := l.chunk(2)
|
|
554
|
+
expect chunks.toStr() == "[[1,2],[3]]"
|
|
555
|
+
|
|
556
|
+
test "chunk splits into sublists of at most size elements each"
|
|
557
|
+
l := makeList(1, 2, 3, 4, 5)
|
|
558
|
+
chunks := l.chunk(2)
|
|
559
|
+
expect chunks.length() == 3
|
|
560
|
+
expect chunks.get(0).unwrap().join(",") == "1,2"
|
|
561
|
+
expect chunks.get(1).unwrap().join(",") == "3,4"
|
|
562
|
+
expect chunks.get(2).unwrap().join(",") == "5"
|
|
563
|
+
expect l.chunk(0).length() == 0
|
|
564
|
+
|
|
565
|
+
test "partition splits into elements matching a predicate, then the rest"
|
|
566
|
+
l := makeList(1, 2, 3, 4, 5, 6)
|
|
567
|
+
parts := l.partition(|v| v % 2 == 0)
|
|
568
|
+
expect parts.length() == 2
|
|
569
|
+
expect parts.get(0).unwrap().join(",") == "2,4,6"
|
|
570
|
+
expect parts.get(1).unwrap().join(",") == "1,3,5"
|