plum

#treesitter#compiler#wasm

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.is_empty() || names.contains(n) {
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!("error '{}' is not part of {}", n, subject_ty))
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:!{t}"),
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(&**t).copied(),
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(&(ty.clone(), vname.to_string())).copied(),
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(&**inner) {
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(&(ety.clone(), call.name.clone())).ok_or_else(|| {
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
+ }