plum

#treesitter#compiler#wasm

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

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


5a9e763Peter 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
Files changed (2) hide show
  1. README.md +2 -1
  2. 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 two new generic params nested inside a closure's return type) remains genuinely unimplemented, same limitation as `Option.andThen`
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
- # verified: a program merely `import`ing `std/list` with these methods
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
- # specialized `add` method on the resulting `List[List[T]]` (e.g.
380
+ # body one for the OUTER `result: List[List[T]]` (matching this
382
- # `List$List$Int`) gets registered with a WRONG/confused parameter type
381
+ # method's own declared return type) and one for an INNER per-chunk
383
- # (`expected List$Int, found Int` and, inconsistently, `expected List$Int,
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
- # found List$List$Int` for the very same key, across different reported
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
- # specialization, possibly compounded by both keeping the SAME bare method
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
- # name (`specializeFn` never mangles an ordinary method's own name).
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
- fun chunk(self) =
396
+ if size <= 0
397
+ return result
390
- todo
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
- fun partition(self) =
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
- todo
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"