plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
2e28ecc
— Peter John
2026-07-20T16:22:48+05:30
test+docs: generics monomorphization complete; extend examples, update known gaps
- README.md +2 -2
- examples/functions.plum +6 -0
- examples/types.plum +6 -0
- plum-checker/tests/checker_tests.rs +27 -0
- plum-wasm-codegen/tests/codegen_tests.rs +92 -0
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.
|
|
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
|
+
}
|