plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
0dc0e16
— Peter John
2026-10-27T00:48:57+05:30
fix: error sets don't split GC types; match arms must be in the error set
plum-checker/src/lib.rs
CHANGED
|
@@ -591,6 +591,8 @@ pub fn plumTypeFromName(name: &str) -> PlumType {
|
|
|
591
591
|
|
|
592
592
|
/// Reserved `TypeEnv` key holding the enclosing function's declared return type.
|
|
593
593
|
const RETURN_KEY: &str = "$return";
|
|
594
|
+
/// Reserved env key set (to a `TNamed(fn name)`) while checking a `fun*` body.
|
|
595
|
+
const STAR_KEY: &str = "$star";
|
|
594
596
|
/// `TypeEnv` key prefix marking a name as a top-level function (a local of the
|
|
595
597
|
/// same name shadows it instead of being a redeclaration).
|
|
596
598
|
const GLOBAL_KEY: &str = "$global:";
|
|
@@ -1867,6 +1869,9 @@ fn checkFn(
|
|
|
1867
1869
|
.unwrap_or(PlumType::TUnit);
|
|
1868
1870
|
// Reserved env key read by `?` (see `inferExpr`'s `Try` arm); closures remove it.
|
|
1869
1871
|
env.insert(RETURN_KEY.to_string(), TypeScheme::mono(declared_ret.clone()));
|
|
1872
|
+
if f.is_star {
|
|
1873
|
+
env.insert(STAR_KEY.to_string(), TypeScheme::mono(PlumType::TNamed(f.name.clone())));
|
|
1874
|
+
}
|
|
1870
1875
|
|
|
1871
1876
|
match &f.body {
|
|
1872
1877
|
ast::FnBody::Expr(e) => match inferExprWith(e, &env, ctx, Some(&declared_ret)) {
|
|
@@ -2739,10 +2744,11 @@ fn checkPattern(
|
|
|
2739
2744
|
let is_upper = |n: &str| n.chars().next().is_some_and(|c| c.is_uppercase());
|
|
2740
2745
|
if let ast::CasePattern::Name(n) = pat {
|
|
2741
2746
|
if is_upper(n) {
|
|
2742
|
-
return if names.
|
|
2747
|
+
return if names.contains(n) {
|
|
2743
2748
|
Ok(())
|
|
2744
2749
|
} else {
|
|
2750
|
+
let set: Vec<&str> = names.iter().map(|s| s.as_str()).collect();
|
|
2745
|
-
Err(format!("
|
|
2751
|
+
Err(format!("'{}' is not in the error set [{}]", n, set.join(", ")))
|
|
2746
2752
|
};
|
|
2747
2753
|
}
|
|
2748
2754
|
}
|
|
@@ -3535,6 +3541,12 @@ fn inferExprInner(
|
|
|
3535
3541
|
ast::Expr::Paren(inner) => inferExprWith(inner, env, ctx, successExpected(expected)),
|
|
3536
3542
|
// `expr?`: the operand's error set must fit the enclosing fn's set.
|
|
3537
3543
|
ast::Expr::Try(inner) => {
|
|
3544
|
+
if let (Some(star), true) = (env.get(STAR_KEY), env.contains_key(RETURN_KEY)) {
|
|
3545
|
+
return Err(format!(
|
|
3546
|
+
"'?' inside fun* '{}': propagation isn't supported in iterators; handle the error with catch",
|
|
3547
|
+
star.body.to_string().trim_start_matches("__star_")
|
|
3548
|
+
));
|
|
3549
|
+
}
|
|
3538
3550
|
let inner_ty = resolveRecorded(&inferExpr(inner, env, ctx)?);
|
|
3539
3551
|
let Some((success, iset)) = errView(&inner_ty) else {
|
|
3540
3552
|
return Err(format!("'?' requires an error union (!T or T?), found {}", inner_ty));
|
plum-checker/tests/error_union_tests.rs
CHANGED
|
@@ -398,3 +398,60 @@ fun f() -> Int =
|
|
|
398
398
|
";
|
|
399
399
|
assert!(check(bad).unwrap_err().contains("declare `-> !T`"));
|
|
400
400
|
}
|
|
401
|
+
|
|
402
|
+
#[test]
|
|
403
|
+
fn match_arm_names_must_be_in_the_error_set() {
|
|
404
|
+
let base = "\
|
|
405
|
+
fun f(n: Int) -> !Int =
|
|
406
|
+
if n < 0
|
|
407
|
+
fail Negative
|
|
408
|
+
if n > 9
|
|
409
|
+
fail TooBig
|
|
410
|
+
n
|
|
411
|
+
|
|
412
|
+
";
|
|
413
|
+
let bad = format!("{}{}", base, "\
|
|
414
|
+
fun g(n: Int) -> Int =
|
|
415
|
+
f(n) catch e
|
|
416
|
+
match e
|
|
417
|
+
Negative => 0
|
|
418
|
+
TooBig => 9
|
|
419
|
+
Bogus => 1
|
|
420
|
+
");
|
|
421
|
+
let msg = check(&bad).unwrap_err();
|
|
422
|
+
assert!(msg.contains("'Bogus' is not in the error set [Negative, TooBig]"), "{}", msg);
|
|
423
|
+
// An empty set (a union that can't fail) accepts no arm names either.
|
|
424
|
+
let empty = "\
|
|
425
|
+
fun safe(n: Int) -> !Int =
|
|
426
|
+
n
|
|
427
|
+
|
|
428
|
+
fun g(n: Int) -> Int =
|
|
429
|
+
safe(n) catch e
|
|
430
|
+
match e
|
|
431
|
+
Anything => 0
|
|
432
|
+
_ => 1
|
|
433
|
+
";
|
|
434
|
+
let msg = check(empty).unwrap_err();
|
|
435
|
+
assert!(msg.contains("'Anything' is not in the error set []"), "{}", msg);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
#[test]
|
|
439
|
+
fn try_inside_star_fn_has_a_specific_message() {
|
|
440
|
+
let src = "\
|
|
441
|
+
fun f(n: Int) -> !Int =
|
|
442
|
+
if n < 0
|
|
443
|
+
fail Negative
|
|
444
|
+
n
|
|
445
|
+
|
|
446
|
+
fun* gen(n: Int) *Int =
|
|
447
|
+
x := f(n)?
|
|
448
|
+
yield x
|
|
449
|
+
";
|
|
450
|
+
let msg = check(src).unwrap_err();
|
|
451
|
+
assert!(
|
|
452
|
+
msg.contains("'?' inside fun* 'gen': propagation isn't supported in iterators; handle the error with catch"),
|
|
453
|
+
"{}",
|
|
454
|
+
msg
|
|
455
|
+
);
|
|
456
|
+
assert!(!msg.contains("declare `-> !T`"), "{}", msg);
|
|
457
|
+
}
|
plum-wasm-codegen/src/lib.rs
CHANGED
|
@@ -870,10 +870,27 @@ fn valueHelper(key: &str) -> u32 {
|
|
|
870
870
|
VALUE_HELPERS.with(|h| *h.borrow().get(key).unwrap_or_else(|| panic!("internal codegen error: missing helper {key}")))
|
|
871
871
|
}
|
|
872
872
|
|
|
873
|
+
/// Error sets don't change a type's runtime shape (a `!T` is `(T leaves, code)`
|
|
874
|
+
/// whatever its set), so GC-type registry keys and instantiation keys erase them:
|
|
875
|
+
/// `List[!Int [A]]` and `List[!Int]` are one wasm type family. The checker keeps
|
|
876
|
+
/// the precise sets; only codegen identity canonicalizes.
|
|
877
|
+
fn canonTy(ty: &PlumType) -> PlumType {
|
|
878
|
+
use PlumType::*;
|
|
879
|
+
match ty {
|
|
880
|
+
// `T?` over a reference type is a nullable ref (a different shape): keep it.
|
|
881
|
+
TErr(t, set) if nullableErr(ty).is_some() => TErr(Box::new(canonTy(t)), set.clone()),
|
|
882
|
+
TErr(t, _) => TErr(Box::new(canonTy(t)), plum_checker::types::ErrSet::Inferred(String::new())),
|
|
883
|
+
TApp(n, args) => TApp(n.clone(), args.iter().map(canonTy).collect()),
|
|
884
|
+
TFun(ps, r) => TFun(ps.iter().map(canonTy).collect(), Box::new(canonTy(r))),
|
|
885
|
+
TVariadic(i) => TVariadic(Box::new(canonTy(i))),
|
|
886
|
+
_ => ty.clone(),
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
873
890
|
fn unboxKey(ty: &PlumType) -> String {
|
|
874
891
|
match ty {
|
|
875
892
|
PlumType::TNamed(n) => format!("unbox:{n}"),
|
|
876
|
-
PlumType::TErr(t, _) => format!("unbox:!{
|
|
893
|
+
PlumType::TErr(t, _) => format!("unbox:!{}", canonTy(t)),
|
|
877
894
|
_ => unreachable!("emitUnbox on non-value type"),
|
|
878
895
|
}
|
|
879
896
|
}
|
|
@@ -959,7 +976,7 @@ impl GcTypeRegistry {
|
|
|
959
976
|
.get(n)
|
|
960
977
|
.or_else(|| self.class_type_idx.get(n))
|
|
961
978
|
.copied(),
|
|
962
|
-
PlumType::TApp(..) => self.app_super.get(ty).copied(),
|
|
979
|
+
PlumType::TApp(..) => self.app_super.get(&canonTy(ty)).copied(),
|
|
963
980
|
_ => None,
|
|
964
981
|
}
|
|
965
982
|
}
|
|
@@ -967,15 +984,15 @@ impl GcTypeRegistry {
|
|
|
967
984
|
pub fn classIdx(&self, ty: &PlumType) -> Option<u32> {
|
|
968
985
|
match ty {
|
|
969
986
|
PlumType::TNamed(n) => self.class_type_idx.get(n).copied(),
|
|
970
|
-
PlumType::TApp(..) => self.app_class.get(ty).copied(),
|
|
987
|
+
PlumType::TApp(..) => self.app_class.get(&canonTy(ty)).copied(),
|
|
971
|
-
PlumType::TErr(t, _) => self.err_box.get(&
|
|
988
|
+
PlumType::TErr(t, _) => self.err_box.get(&canonTy(t)).copied(),
|
|
972
989
|
_ => None,
|
|
973
990
|
}
|
|
974
991
|
}
|
|
975
992
|
/// Struct index of variant `vname` of enum type `ty`.
|
|
976
993
|
pub fn variantIdx(&self, ty: &PlumType, vname: &str) -> Option<u32> {
|
|
977
994
|
match ty {
|
|
978
|
-
PlumType::TApp(..) => self.app_variant.get(&(
|
|
995
|
+
PlumType::TApp(..) => self.app_variant.get(&(canonTy(ty), vname.to_string())).copied(),
|
|
979
996
|
_ => self.variant_type_idx.get(vname).copied(),
|
|
980
997
|
}
|
|
981
998
|
}
|
|
@@ -1025,7 +1042,7 @@ fn plumTypeToGcValtype(t: &PlumType, registry: &GcTypeRegistry) -> ValType {
|
|
|
1025
1042
|
// An error value (`catch e`) is its i32 code.
|
|
1026
1043
|
PlumType::TErrName(_) => ValType::I32,
|
|
1027
1044
|
// A heap slot holds a `!T` as its box struct `{T leaves..., code}`.
|
|
1028
|
-
PlumType::TErr(inner, _) => match registry.err_box.get(&
|
|
1045
|
+
PlumType::TErr(inner, _) => match registry.err_box.get(&canonTy(inner)) {
|
|
1029
1046
|
Some(idx) => gcRef(*idx),
|
|
1030
1047
|
None => ValType::Ref(RefType::ANYREF),
|
|
1031
1048
|
},
|
|
@@ -1742,6 +1759,8 @@ impl<'a> TypeCollector<'a> {
|
|
|
1742
1759
|
if hasFreeTy(t) {
|
|
1743
1760
|
return;
|
|
1744
1761
|
}
|
|
1762
|
+
let canon = canonTy(t);
|
|
1763
|
+
let t = &canon;
|
|
1745
1764
|
match t {
|
|
1746
1765
|
PlumType::TApp(n, args) => {
|
|
1747
1766
|
for a in args {
|
|
@@ -1905,6 +1924,25 @@ fn compileSourceInner(
|
|
|
1905
1924
|
}
|
|
1906
1925
|
|
|
1907
1926
|
// ---- collect the concrete generic types the program uses ----
|
|
1927
|
+
VALUE_ENUMS.with(|v| {
|
|
1928
|
+
let mut v = v.borrow_mut();
|
|
1929
|
+
v.clear();
|
|
1930
|
+
for item in &source.items {
|
|
1931
|
+
if let ast::Item::Enum(e) = item {
|
|
1932
|
+
if e.isValue && e.variants.len() == 1 && e.generics.is_empty() {
|
|
1933
|
+
let var = &e.variants[0];
|
|
1934
|
+
v.insert(
|
|
1935
|
+
e.name.clone(),
|
|
1936
|
+
var.field_names
|
|
1937
|
+
.iter()
|
|
1938
|
+
.cloned()
|
|
1939
|
+
.zip(var.fields.iter().map(plum_checker::plumTypeFromAst))
|
|
1940
|
+
.collect(),
|
|
1941
|
+
);
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
});
|
|
1908
1946
|
let mut tc = TypeCollector {
|
|
1909
1947
|
apps: Vec::new(),
|
|
1910
1948
|
seen: HashSet::new(),
|
|
@@ -1957,25 +1995,6 @@ fn compileSourceInner(
|
|
|
1957
1995
|
|
|
1958
1996
|
let mut module = WasmModule::new();
|
|
1959
1997
|
|
|
1960
|
-
VALUE_ENUMS.with(|v| {
|
|
1961
|
-
let mut v = v.borrow_mut();
|
|
1962
|
-
v.clear();
|
|
1963
|
-
for item in &source.items {
|
|
1964
|
-
if let ast::Item::Enum(e) = item {
|
|
1965
|
-
if e.isValue && e.variants.len() == 1 && e.generics.is_empty() {
|
|
1966
|
-
let var = &e.variants[0];
|
|
1967
|
-
v.insert(
|
|
1968
|
-
e.name.clone(),
|
|
1969
|
-
var.field_names
|
|
1970
|
-
.iter()
|
|
1971
|
-
.cloned()
|
|
1972
|
-
.zip(var.fields.iter().map(plum_checker::plumTypeFromAst))
|
|
1973
|
-
.collect(),
|
|
1974
|
-
);
|
|
1975
|
-
}
|
|
1976
|
-
}
|
|
1977
|
-
}
|
|
1978
|
-
});
|
|
1979
1998
|
VALUE_BLOCK_TYPES.with(|m| m.borrow_mut().clear());
|
|
1980
1999
|
VALUE_HELPERS.with(|m| m.borrow_mut().clear());
|
|
1981
2000
|
|
|
@@ -6701,7 +6720,7 @@ fn compileExprCore(
|
|
|
6701
6720
|
match ctx.enum_variants.get(n) {
|
|
6702
6721
|
Some(info) if info.field_types.is_empty() => {
|
|
6703
6722
|
let ety = enumTypeOfExpr(expr, info, ctx);
|
|
6704
|
-
let global_idx = *ctx.singleton_globals.get(&(ety, n.clone()))
|
|
6723
|
+
let global_idx = *ctx.singleton_globals.get(&(canonTy(&ety), n.clone()))
|
|
6705
6724
|
.unwrap_or_else(|| panic!("internal codegen error: payload-free variant '{}' has no singleton global", n));
|
|
6706
6725
|
Instruction::GlobalGet(global_idx).encode(body);
|
|
6707
6726
|
}
|
|
@@ -7339,7 +7358,7 @@ fn compileVariantConstruction(
|
|
|
7339
7358
|
if info.field_types.is_empty() {
|
|
7340
7359
|
// Pre-allocated singleton (this migration plan's Decision 2) — not a fresh
|
|
7341
7360
|
// struct.new per reference.
|
|
7342
|
-
let global_idx = *ctx.singleton_globals.get(&(
|
|
7361
|
+
let global_idx = *ctx.singleton_globals.get(&(canonTy(&ety), call.name.clone())).ok_or_else(|| {
|
|
7343
7362
|
format!(
|
|
7344
7363
|
"codegen: payload-free variant '{}' has no singleton global",
|
|
7345
7364
|
call.name
|
plum-wasm-codegen/tests/codegen_tests.rs
CHANGED
|
@@ -2750,3 +2750,52 @@ fun main() -> Int =
|
|
|
2750
2750
|
";
|
|
2751
2751
|
assert_eq!(runWithCrash("nullvalue", src).0.unwrap(), 400 + 50 + 9);
|
|
2752
2752
|
}
|
|
2753
|
+
|
|
2754
|
+
#[test]
|
|
2755
|
+
fn list_of_error_unions_across_sets_valid_wasm() {
|
|
2756
|
+
let src = "\
|
|
2757
|
+
import std/Str
|
|
2758
|
+
import std/Number
|
|
2759
|
+
import std/List
|
|
2760
|
+
|
|
2761
|
+
fun chk(n: Int) -> !Int =
|
|
2762
|
+
if n < 0
|
|
2763
|
+
fail Neg
|
|
2764
|
+
n
|
|
2765
|
+
|
|
2766
|
+
fun total(xs: List[!Int [Neg]]) -> Int =
|
|
2767
|
+
a := xs.at(0) catch 0
|
|
2768
|
+
b := xs.at(1) catch 100
|
|
2769
|
+
a + b
|
|
2770
|
+
|
|
2771
|
+
fun main() -> Int =
|
|
2772
|
+
xs := List(chk(4))
|
|
2773
|
+
xs.push(chk(-1))
|
|
2774
|
+
total(xs)
|
|
2775
|
+
";
|
|
2776
|
+
assert_eq!(runWithCrash("listsets", src).0.unwrap(), 104);
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2779
|
+
#[test]
|
|
2780
|
+
fn map_value_error_union_across_sets() {
|
|
2781
|
+
let src = "\
|
|
2782
|
+
import std/Str
|
|
2783
|
+
import std/Number
|
|
2784
|
+
import std/Map
|
|
2785
|
+
|
|
2786
|
+
fun chk(n: Int) -> !Int =
|
|
2787
|
+
if n < 0
|
|
2788
|
+
fail Neg
|
|
2789
|
+
n
|
|
2790
|
+
|
|
2791
|
+
fun look(m: Map[Int, !Int [Neg]]) -> Int =
|
|
2792
|
+
v := m.get(1) catch chk(-1)
|
|
2793
|
+
v catch 0
|
|
2794
|
+
|
|
2795
|
+
fun main() -> Int =
|
|
2796
|
+
m := Map[Int, !Int]()
|
|
2797
|
+
m.set(1, chk(7))
|
|
2798
|
+
look(m)
|
|
2799
|
+
";
|
|
2800
|
+
assert_eq!(runWithCrash("mapsets", src).0.unwrap(), 7);
|
|
2801
|
+
}
|