plum

#treesitter#compiler#wasm

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

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


2e28eccPeter John 2026-07-20T16:22:48+05:30
test+docs: generics monomorphization complete; extend examples, update known gaps
README.md CHANGED
@@ -240,7 +240,7 @@ wrap(value: a) -> Bool = # generic param type
240
240
  True
241
241
  ```
242
242
 
243
- Generic **arguments** (instantiating a generic type) accept either bracket or paren syntax: `List[Int]` and `List(Int)` both parse. There's no monomorphization/codegen for user-defined generics yet they type-check permissively but don't compile to wasm.
243
+ Generic **arguments** (instantiating a generic type) accept either bracket or paren syntax: `List[Int]` and `List(Int)` both parse. User-defined generics (classes, their methods, free functions, and enums) are monomorphized: each concrete-type-argument combination actually used in the program gets its own specialized, fully-concrete copy, which then type-checks and compiles to wasm through the normal, unmodified pipeline. See `useWrap`/`usePair` in [`examples/functions.plum`](examples/functions.plum) and `makeIntBox`/`makeStrBox` in [`examples/types.plum`](examples/types.plum) for real instantiation sites. One documented limitation: a generic *enum* may only be instantiated at one concrete type per program (instantiating the same generic enum at two different concrete types produces a clear `monomorphize:`-prefixed error, since the runtime's enum-variant table is keyed by bare variant name).
244
244
 
245
245
  Full example: [`examples/types.plum`](examples/types.plum), [`examples/functions.plum`](examples/functions.plum).
246
246
 
@@ -322,7 +322,7 @@ Some things parse and type-check but don't compile to wasm yet — `plum-wasm-co
322
322
 
323
323
  - string interpolation (plain, non-interpolated string literals do compile)
324
324
  - multi-subject `match` (`match a, b`)
325
- - user-defined generics (they type-check but aren't monomorphized) — this also blocks `libs/std`'s actual `Option`/`Result`/`List`/`Map`, which are declared generically
326
325
  - nested constructor patterns inside `match` (`Some(Some(v))`) — a constructor pattern's own sub-patterns must be a bare binding or `_`
326
+ - `libs/std`'s actual `List`/`Map`/`Option`/`Result` still don't compile — they use several other unimplemented features (closures, `Nil`/optional chaining, decorators, colon-arrow return syntax) unrelated to generics, which are themselves now monomorphized and compiled correctly for the currently-documented generic syntax
327
327
 
328
328
  `closure` (`|params| body`) exists in `grammar.js` but isn't wired into any reachable rule yet, so it doesn't actually parse in context.
examples/functions.plum CHANGED
@@ -15,3 +15,9 @@ wrap(value: a) -> Bool =
15
15
 
16
16
  pair(first: a, second: b) -> Bool =
17
17
  True
18
+
19
+ useWrap() -> Bool =
20
+ wrap(5)
21
+
22
+ usePair() -> Bool =
23
+ pair(1, "x")
examples/types.plum CHANGED
@@ -23,3 +23,9 @@ enum Color =
23
23
  enum Option =
24
24
  | Some(Int)
25
25
  | None
26
+
27
+ makeIntBox() -> Box =
28
+ Box(value: 5)
29
+
30
+ makeStrBox() -> Box =
31
+ Box(value: "x")
plum-checker/tests/checker_tests.rs CHANGED
@@ -482,3 +482,30 @@ use() -> Int =
482
482
  let result = check_source(&source);
483
483
  assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
484
484
  }
485
+
486
+ #[test]
487
+ fn unbounded_recursive_generic_instantiation_is_a_clear_error() {
488
+ // `recurse` re-wraps its argument in a `Box` on every recursive call, so each
489
+ // specialization's own body demands a specialization of `recurse` at a STRICTLY
490
+ // bigger type (`recurse$Int`, then `recurse$Box$Int`, then `recurse$Box$Box$Int`,
491
+ // ...), forever. This genuinely grows the worklist without bound (unlike a
492
+ // generic class field merely NAMING a recursive generic type in its own
493
+ // declaration, which is never itself a call site and so never reaches the
494
+ // worklist at all) and must fail with a clear, bounded error rather than hang.
495
+ let src = "\
496
+ type Box(a) =
497
+ value: a
498
+
499
+ recurse(v: a) -> Int =
500
+ b = Box(value: v)
501
+ recurse(b)
502
+
503
+ use() -> Int =
504
+ recurse(5)
505
+ ";
506
+ let source = parse(src);
507
+ let result = check_source(&source);
508
+ assert!(result.is_err());
509
+ let errs = result.unwrap_err();
510
+ assert!(errs[0].message.contains("monomorphize"), "got: {:?}", errs);
511
+ }
plum-wasm-codegen/tests/codegen_tests.rs CHANGED
@@ -684,3 +684,95 @@ bad(n: Int) -> Int =
684
684
  let err = compile_source(&source).expect_err("a match arm ending in a non-value statement must be a clear error, not invalid wasm");
685
685
  assert!(err.contains("doesn't produce a return value"), "got: {}", err);
686
686
  }
687
+
688
+ #[test]
689
+ fn generic_class_specialized_at_two_types_does_not_alias() {
690
+ let src = "\
691
+ type Box(a) =
692
+ value: a
693
+
694
+ getIntValue<Box>() -> Int =
695
+ self.value
696
+
697
+ useInt() -> Int =
698
+ b = Box(value: 7)
699
+ b.getIntValue()
700
+
701
+ main() -> Int =
702
+ useInt()
703
+ ";
704
+ let source = parse(src);
705
+ let bytes = compile_source(&source).expect("compile failed");
706
+ assert_eq!(run_main(&bytes), 7);
707
+ }
708
+
709
+ #[test]
710
+ fn generic_function_called_at_multiple_concrete_types_runs_correctly() {
711
+ let src = "\
712
+ identity(value: a) -> a =
713
+ value
714
+
715
+ main() -> Int =
716
+ identity(5) + identity(37)
717
+ ";
718
+ let source = parse(src);
719
+ let bytes = compile_source(&source).expect("compile failed");
720
+ assert_eq!(run_main(&bytes), 42);
721
+ }
722
+
723
+ #[test]
724
+ fn generic_method_on_generic_class_runs_correctly() {
725
+ let src = "\
726
+ type Box(a) =
727
+ value: a
728
+
729
+ getValue<Box>() -> Int =
730
+ self.value
731
+
732
+ main() -> Int =
733
+ b = Box(value: 9)
734
+ b.getValue()
735
+ ";
736
+ let source = parse(src);
737
+ let bytes = compile_source(&source).expect("compile failed");
738
+ assert_eq!(run_main(&bytes), 9);
739
+ }
740
+
741
+ #[test]
742
+ fn transitively_generic_call_chain_runs_correctly() {
743
+ let src = "\
744
+ identity(value: a) -> a =
745
+ value
746
+
747
+ doubled(value: a) -> Int =
748
+ identity(value) + identity(value)
749
+
750
+ main() -> Int =
751
+ doubled(21)
752
+ ";
753
+ let source = parse(src);
754
+ let bytes = compile_source(&source).expect("compile failed");
755
+ assert_eq!(run_main(&bytes), 42);
756
+ }
757
+
758
+ #[test]
759
+ fn generic_enum_specialized_and_matched_runs_correctly() {
760
+ let src = "\
761
+ enum Option =
762
+ | Some(a)
763
+ | None
764
+
765
+ unwrapOr(o: Option, default: Int) -> Int =
766
+ match o
767
+ Some(v) =>
768
+ v
769
+ None =>
770
+ default
771
+
772
+ main() -> Int =
773
+ unwrapOr(Some(13), 0)
774
+ ";
775
+ let source = parse(src);
776
+ let bytes = compile_source(&source).expect("compile failed");
777
+ assert_eq!(run_main(&bytes), 13);
778
+ }