plum

#treesitter#compiler#wasm

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

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


a8f2482 — Peter John 2026-10-26T23:46:13+05:30
feat(codegen): Zig-style error unions: !T multi-value lowering, fail/?/!/catch/?., boxed heap slots, nullable interop
plum-checker/src/lib.rs CHANGED
@@ -529,8 +529,51 @@ fn recordBind(name: &str, ty: &PlumType) {
529
529
  pub const TRY_WRAP_OK: usize = 1;
530
530
  pub const TRY_WRAP_ERR: usize = 2;
531
531
  pub const TRY_RELAYOUT: usize = 3;
532
+ /// Error-union coercions (expected `!T`): `+4` a plain `T` becomes the success value,
533
+ /// `+5` a nullable `Try` becomes `!T`; `+6` (expected a nullable `Try`) a `!T` becomes null/value.
534
+ pub const ERR_WRAP: usize = 4;
535
+ pub const ERR_FROM_NULLABLE: usize = 5;
536
+ pub const ERR_TO_NULLABLE: usize = 6;
537
+
538
+ fn recordErrCoercion(expr: &ast::Expr, actual: &PlumType, expected: &PlumType, ctx: &CheckCtx) -> bool {
539
+ let kind = match (expected, actual) {
540
+ (_, PlumType::TVar(_) | PlumType::TParam(_)) => return false,
541
+ (PlumType::TErr(inner, _), a) => {
542
+ if matches!(a, PlumType::TErr(..)) {
543
+ None
544
+ } else if nullableTryPayload(a).is_some() {
545
+ Some(ERR_FROM_NULLABLE)
546
+ } else if isTryType(a) {
547
+ None
548
+ } else if unify(inner, a, ctx).is_ok() {
549
+ Some(ERR_WRAP)
550
+ } else {
551
+ None
552
+ }
553
+ }
554
+ (e, PlumType::TErr(..)) if nullableTryPayload(e).is_some() => Some(ERR_TO_NULLABLE),
555
+ _ => return false,
556
+ };
557
+ let ptr = expr as *const ast::Expr as usize;
558
+ RECORD.with(|r| {
559
+ if let Some(m) = r.borrow_mut().as_mut() {
560
+ for k in [TRY_WRAP_OK, TRY_WRAP_ERR, TRY_RELAYOUT, ERR_WRAP, ERR_FROM_NULLABLE, ERR_TO_NULLABLE] {
561
+ m.remove(&(ptr + k));
562
+ }
563
+ if let Some(k) = kind {
564
+ m.insert(ptr + k, expected.clone());
565
+ }
566
+ }
567
+ });
568
+ true
569
+ }
532
570
 
533
571
  fn recordTryCoercion(expr: &ast::Expr, actual: &PlumType, expected: Option<&PlumType>, ctx: &CheckCtx) {
572
+ if let Some(e) = expected {
573
+ if recordErrCoercion(expr, actual, e, ctx) {
574
+ return;
575
+ }
576
+ }
534
577
  let Some(exp @ PlumType::TApp(n, args)) = expected else {
535
578
  return;
536
579
  };
plum-wasm-codegen/src/lib.rs CHANGED
@@ -584,8 +584,9 @@ fn sourceUsesCrash(dbg: &str) -> bool {
584
584
  }
585
585
  i += 1;
586
586
  }
587
+ b'C' | b'B'
587
- b'C' if dbg[i..].starts_with("Crash(")
588
+ if (dbg[i..].starts_with("Crash(") || dbg[i..].starts_with("Bang("))
588
- && (i == 0 || matches!(b[i - 1], b'[' | b'(' | b' ')) =>
589
+ && (i == 0 || matches!(b[i - 1], b'[' | b'(' | b' ')) =>
589
590
  {
590
591
  return true
591
592
  }
@@ -595,6 +596,18 @@ fn sourceUsesCrash(dbg: &str) -> bool {
595
596
  false
596
597
  }
597
598
 
599
+ /// The closure literal of an error-union `a.andThen(|x| ...)` (what `a?.b` desugars to).
600
+ fn andThenClosure<'e>(a: &'e ast::AttributeExpr, call: &'e ast::FnCall) -> Option<&'e ast::Closure> {
601
+ let _ = a;
602
+ if call.name != "andThen" || call.args.len() != 1 {
603
+ return None;
604
+ }
605
+ match argExprOf(&call.args[0]) {
606
+ ast::Expr::Closure(cl) if cl.params.len() == 1 => Some(cl),
607
+ _ => None,
608
+ }
609
+ }
610
+
598
611
  fn tagId(name: &str) -> i32 {
599
612
  CURRENT_TAG_IDS.with(|c| {
600
613
  let mut m = c.borrow_mut();
@@ -796,11 +809,59 @@ thread_local! {
796
809
  static VALUE_BLOCK_TYPES: RefCell<HashMap<u32, u32>> = RefCell::new(HashMap::new());
797
810
  /// Helper function indices: `unbox:Vec2`, `eq:Vec2`, `proj:Vec2.x`.
798
811
  static VALUE_HELPERS: RefCell<HashMap<String, u32>> = RefCell::new(HashMap::new());
812
+ /// Error-union facts from the checker for the program being compiled.
813
+ static ERR_INFO: RefCell<ErrInfo> = RefCell::new(ErrInfo::default());
814
+ }
815
+
816
+ #[derive(Default)]
817
+ struct ErrInfo {
818
+ file: String,
819
+ sites: HashMap<usize, plum_checker::ErrSite>,
820
+ codes: std::collections::BTreeMap<String, i32>,
821
+ }
822
+
823
+ fn errSite(expr: &ast::Expr) -> Option<plum_checker::ErrSite> {
824
+ ERR_INFO.with(|e| e.borrow().sites.get(&(expr as *const ast::Expr as usize)).cloned())
825
+ }
826
+
827
+ fn errCode(name: &str) -> Result<i32, String> {
828
+ ERR_INFO
829
+ .with(|e| e.borrow().codes.get(name).copied())
830
+ .ok_or_else(|| format!("internal codegen error: no error code for '{}'", name))
831
+ }
832
+
833
+ /// Scratch local (named, reserved by `Collector`) holding an error-union operand.
834
+ fn errScratch(expr: &ast::Expr) -> String {
835
+ format!("errtmp#{}", expr as *const ast::Expr as usize)
836
+ }
837
+
838
+ fn errScratchKey(expr: &ast::Expr, kind: usize) -> String {
839
+ format!("errtmp#{}", expr as *const ast::Expr as usize + kind)
840
+ }
841
+
842
+ /// The wasm leaf zero for an "undefined" value slot.
843
+ fn emitZeroLeaf(vt: ValType, body: &mut Vec<u8>) {
844
+ pushSelfPlaceholder(vt, body);
845
+ }
846
+
847
+ fn emitZeros(ty: &PlumType, body: &mut Vec<u8>) {
848
+ for vt in withGcTypes(|g| flatValtypes(ty, g)) {
849
+ emitZeroLeaf(vt, body);
850
+ }
799
851
  }
800
852
 
801
853
  fn valueFieldsOf(ty: &PlumType) -> Option<Vec<(String, PlumType)>> {
802
854
  match ty {
803
855
  PlumType::TNamed(n) => VALUE_ENUMS.with(|v| v.borrow().get(n).cloned()),
856
+ // `!T` is `(T leaves..., code: i32)`; a Unit success has only the code.
857
+ PlumType::TErr(t, _) => {
858
+ let mut f = Vec::new();
859
+ if !matches!(**t, PlumType::TUnit) {
860
+ f.push(("$value".to_string(), (**t).clone()));
861
+ }
862
+ f.push(("$code".to_string(), PlumType::TBool));
863
+ Some(f)
864
+ }
804
865
  _ => None,
805
866
  }
806
867
  }
@@ -872,10 +933,17 @@ fn valueHelper(key: &str) -> u32 {
872
933
  VALUE_HELPERS.with(|h| *h.borrow().get(key).unwrap_or_else(|| panic!("internal codegen error: missing helper {key}")))
873
934
  }
874
935
 
936
+ fn unboxKey(ty: &PlumType) -> String {
937
+ match ty {
938
+ PlumType::TNamed(n) => format!("unbox:{n}"),
939
+ PlumType::TErr(t, _) => format!("unbox:!{t}"),
940
+ _ => unreachable!("emitUnbox on non-value type"),
941
+ }
942
+ }
943
+
875
944
  /// A heap box reference on the stack -> flat leaves.
876
945
  fn emitUnbox(ty: &PlumType, body: &mut Vec<u8>) {
877
- let PlumType::TNamed(n) = ty else { unreachable!("emitUnbox on non-value type") };
878
- Instruction::Call(valueHelper(&format!("unbox:{n}"))).encode(body);
946
+ Instruction::Call(valueHelper(&unboxKey(ty))).encode(body);
879
947
  }
880
948
 
881
949
  /// If the value just produced for a heap slot of type `ty` is a value enum, flatten it.
@@ -949,6 +1017,8 @@ pub struct GcTypeRegistry {
949
1017
  pub tag_ref_idx: u32,
950
1018
  /// Helper `(arr: array<i8>, len: i64) -> Str` building `Str{Buffer{arr,len}}`; `None` when the program has no `Str`/`Buffer`.
951
1019
  pub mk_str_fn: Option<u32>,
1020
+ /// `!T` (keyed by `T`) -> its heap box struct `{leaves of T..., code: i32}`.
1021
+ pub err_box: HashMap<PlumType, u32>,
952
1022
  }
953
1023
 
954
1024
  impl GcTypeRegistry {
@@ -969,6 +1039,7 @@ impl GcTypeRegistry {
969
1039
  match ty {
970
1040
  PlumType::TNamed(n) => self.class_type_idx.get(n).copied(),
971
1041
  PlumType::TApp(..) => self.app_class.get(ty).copied(),
1042
+ PlumType::TErr(t, _) => self.err_box.get(&**t).copied(),
972
1043
  _ => None,
973
1044
  }
974
1045
  }
@@ -1024,7 +1095,10 @@ fn plumTypeToGcValtype(t: &PlumType, registry: &GcTypeRegistry) -> ValType {
1024
1095
  },
1025
1096
  PlumType::TTags(_) => gcRef(registry.tag_type_idx),
1026
1097
  // Lowered by the error-union codegen (E3); until then, as its success type.
1027
- PlumType::TErr(inner, _) => plumTypeToGcValtype(inner, registry),
1098
+ PlumType::TErr(inner, _) => match registry.err_box.get(&**inner) {
1099
+ Some(idx) => gcRef(*idx),
1100
+ None => ValType::Ref(RefType::ANYREF),
1101
+ },
1028
1102
  PlumType::TFun(_, _) | PlumType::TVariadic(_) => ValType::Ref(RefType::ANYREF),
1029
1103
  PlumType::TParam(_) => ValType::Ref(RefType::ANYREF),
1030
1104
  PlumType::TVar(_) | PlumType::TUnit => ValType::I64,
@@ -1063,6 +1137,7 @@ fn buildGcTypeRegistry(
1063
1137
  enum_variants: &EnumVariants,
1064
1138
  enum_params: &EnumParams,
1065
1139
  apps: &[PlumType],
1140
+ errs: &[PlumType],
1066
1141
  ) -> GcTypeRegistry {
1067
1142
  enum Slot {
1068
1143
  Str,
@@ -1074,6 +1149,7 @@ fn buildGcTypeRegistry(
1074
1149
  TagF64,
1075
1150
  TagI32,
1076
1151
  TagRef,
1152
+ ErrBox(PlumType),
1077
1153
  }
1078
1154
  let mut slots: Vec<Slot> = vec![Slot::Str];
1079
1155
  let mut reg = GcTypeRegistry {
@@ -1095,6 +1171,7 @@ fn buildGcTypeRegistry(
1095
1171
  tag_i32_idx: 0,
1096
1172
  tag_ref_idx: 0,
1097
1173
  mk_str_fn: None,
1174
+ err_box: HashMap::new(),
1098
1175
  };
1099
1176
  let mut enum_decls: HashMap<String, &ast::Enum> = HashMap::new();
1100
1177
  let mut instances: Vec<PlumType> = Vec::new();
@@ -1164,6 +1241,13 @@ fn buildGcTypeRegistry(
1164
1241
  reg.tag_ref_idx = slots.len() as u32;
1165
1242
  slots.push(Slot::TagRef);
1166
1243
 
1244
+ for t in errs {
1245
+ if !reg.err_box.contains_key(t) {
1246
+ reg.err_box.insert(t.clone(), slots.len() as u32);
1247
+ slots.push(Slot::ErrBox(t.clone()));
1248
+ }
1249
+ }
1250
+
1167
1251
  // Native arrays: one wasm array type per distinct element representation,
1168
1252
  // innermost (least nested) element types first.
1169
1253
  arrays.sort_by_key(arrayDepth);
@@ -1259,6 +1343,23 @@ fn buildGcTypeRegistry(
1259
1343
  },
1260
1344
  }
1261
1345
  }
1346
+ Slot::ErrBox(t) => {
1347
+ let ty = PlumType::TErr(Box::new(t.clone()), plum_checker::types::ErrSet::Inferred(String::new()));
1348
+ SubType {
1349
+ is_final: true,
1350
+ supertype_idx: None,
1351
+ composite_type: CompositeType {
1352
+ inner: CompositeInnerType::Struct(StructType {
1353
+ fields: flatValtypes(&ty, &reg)
1354
+ .into_iter()
1355
+ .map(|vt| FieldType { element_type: StorageType::Val(vt), mutable: true })
1356
+ .collect::<Vec<_>>()
1357
+ .into(),
1358
+ }),
1359
+ shared: false,
1360
+ },
1361
+ }
1362
+ }
1262
1363
  Slot::Tag => SubType {
1263
1364
  is_final: false,
1264
1365
  supertype_idx: None,
@@ -1763,6 +1864,7 @@ struct TypeCollector<'a> {
1763
1864
  funs_seen: HashSet<PlumType>,
1764
1865
  variadics: Vec<PlumType>,
1765
1866
  variadics_seen: HashSet<PlumType>,
1867
+ errs: Vec<PlumType>,
1766
1868
  enum_variants: &'a EnumVariants,
1767
1869
  enum_params: &'a EnumParams,
1768
1870
  eg: &'a EnumGenerics,
@@ -1822,6 +1924,12 @@ impl<'a> TypeCollector<'a> {
1822
1924
  self.visit(v);
1823
1925
  }
1824
1926
  }
1927
+ PlumType::TErr(inner, _) => {
1928
+ self.visit(inner);
1929
+ if !self.errs.contains(inner) {
1930
+ self.errs.push((**inner).clone());
1931
+ }
1932
+ }
1825
1933
  _ => {}
1826
1934
  }
1827
1935
  }
@@ -1847,8 +1955,11 @@ fn compileSourceInner(
1847
1955
  call_inst,
1848
1956
  op_calls,
1849
1957
  lowered,
1958
+ err_sites,
1959
+ error_codes,
1850
1960
  ..
1851
1961
  } = typed;
1962
+ ERR_INFO.with(|e| *e.borrow_mut() = ErrInfo { file: source.module.as_ref().map(|m| m.name.clone()).unwrap_or_else(|| "main.plum".to_string()), sites: err_sites, codes: error_codes });
1852
1963
  // The types are keyed by the addresses of the checker's lowered program.
1853
1964
  let source: &ast::Source = lowered.as_deref().unwrap_or(source);
1854
1965
  // Per-instantiation expression types, split out by instantiation id.
@@ -1939,6 +2050,7 @@ fn compileSourceInner(
1939
2050
  funs_seen: HashSet::new(),
1940
2051
  variadics: Vec::new(),
1941
2052
  variadics_seen: HashSet::new(),
2053
+ errs: Vec::new(),
1942
2054
  enum_variants: &enum_variants,
1943
2055
  enum_params: &enum_params,
1944
2056
  eg: &eg,
@@ -2041,7 +2153,7 @@ fn compileSourceInner(
2041
2153
  VALUE_BLOCK_TYPES.with(|m| m.borrow_mut().clear());
2042
2154
  VALUE_HELPERS.with(|m| m.borrow_mut().clear());
2043
2155
 
2044
- let mut gc_types = buildGcTypeRegistry(&mut module, source, &enum_variants, &enum_params, &tc.apps);
2156
+ let mut gc_types = buildGcTypeRegistry(&mut module, source, &enum_variants, &enum_params, &tc.apps, &tc.errs);
2045
2157
  CURRENT_GC_TYPES.with(|c| *c.borrow_mut() = Some(gc_types.clone()));
2046
2158
  let value_names: Vec<String> = {
2047
2159
  let mut v: Vec<String> = VALUE_ENUMS.with(|v| v.borrow().keys().cloned().collect());
@@ -2055,6 +2167,14 @@ fn compileSourceInner(
2055
2167
  let bt = module.addType(&[], &leaves);
2056
2168
  VALUE_BLOCK_TYPES.with(|m| m.borrow_mut().insert(box_idx, bt));
2057
2169
  }
2170
+ let mut err_boxes: Vec<(PlumType, u32)> = gc_types.err_box.iter().map(|(t, i)| (t.clone(), *i)).collect();
2171
+ err_boxes.sort_by_key(|(_, i)| *i);
2172
+ for (t, box_idx) in &err_boxes {
2173
+ let ty = PlumType::TErr(Box::new(t.clone()), plum_checker::types::ErrSet::Inferred(String::new()));
2174
+ let leaves = flatValtypes(&ty, &gc_types);
2175
+ let bt = module.addType(&[], &leaves);
2176
+ VALUE_BLOCK_TYPES.with(|m| m.borrow_mut().insert(*box_idx, bt));
2177
+ }
2058
2178
 
2059
2179
  CURRENT_CONSTS.with(|c| {
2060
2180
  let mut consts = c.borrow_mut();
@@ -2105,6 +2225,9 @@ fn compileSourceInner(
2105
2225
  continue;
2106
2226
  }
2107
2227
  known_fns.insert(f.name.clone());
2228
+ if matches!(f.returns.as_ref().map(plum_checker::plumTypeFromAstG), Some(PlumType::TErr(..))) {
2229
+ return Err(format!("extern fun '{}' cannot return an error union", f.name));
2230
+ }
2108
2231
  let sig = unitFuncSig(f, &Subst::new(), &eg, &gc_types);
2109
2232
  let param_types = sig.params.clone();
2110
2233
  let ret = sig.ret.clone();
@@ -2250,6 +2373,21 @@ fn compileSourceInner(
2250
2373
  }
2251
2374
  }
2252
2375
 
2376
+ for (t, box_idx) in &err_boxes {
2377
+ let ty = PlumType::TErr(Box::new(t.clone()), plum_checker::types::ErrSet::Inferred(String::new()));
2378
+ let leaves = flatValtypes(&ty, &gc_types);
2379
+ let mut b = vec![0u8];
2380
+ for i in 0..leaves.len() as u32 {
2381
+ Instruction::LocalGet(0).encode(&mut b);
2382
+ Instruction::StructGet { struct_type_index: *box_idx, field_index: i }.encode(&mut b);
2383
+ }
2384
+ Instruction::End.encode(&mut b);
2385
+ let ft = module.addType(&[gcRef(*box_idx)], &leaves);
2386
+ let f = module.addFunction(ft, &b);
2387
+ module.nameFunction(f, &format!("unbox.!{t}"));
2388
+ VALUE_HELPERS.with(|h| h.borrow_mut().insert(unboxKey(&ty), f));
2389
+ }
2390
+
2253
2391
  // Pre-allocate one instance of every payload-free variant of every enum instance.
2254
2392
  let mut singleton_globals: HashMap<(PlumType, String), u32> = HashMap::new();
2255
2393
  let mut start_body = Vec::new();
@@ -3450,7 +3588,15 @@ impl<'a, 't> ClosureWalker<'a, 't> {
3450
3588
 
3451
3589
  fn walkExpr(&mut self, expr: &'a ast::Expr) {
3452
3590
  match expr {
3453
- ast::Expr::Yield(_) | ast::Expr::Bang(_) | ast::Expr::Catch(_) | ast::Expr::Fail(_) => {}
3591
+ ast::Expr::Yield(_) | ast::Expr::Fail(_) => {}
3592
+ ast::Expr::Bang(b) => self.walkExpr(&b.value),
3593
+ ast::Expr::Catch(c) => {
3594
+ self.walkExpr(&c.value);
3595
+ match &c.handler {
3596
+ ast::CatchHandler::Default(d) => self.walkExpr(d),
3597
+ ast::CatchHandler::Block { body, .. } => self.walkBlock(body),
3598
+ }
3599
+ }
3454
3600
  ast::Expr::Closure(cl) => self.recordClosure(expr, cl),
3455
3601
  ast::Expr::Binary(b) => {
3456
3602
  self.walkExpr(&b.left);
@@ -3733,6 +3879,19 @@ fn fvCollectRefsExpr(
3733
3879
  }
3734
3880
  ast::Expr::Not(inner) => fvCollectRefsExpr(inner, bound, seen, free, env, fn_decls, fn_locals),
3735
3881
  ast::Expr::Try(inner) => fvCollectRefsExpr(inner, bound, seen, free, env, fn_decls, fn_locals),
3882
+ ast::Expr::Bang(b) => fvCollectRefsExpr(&b.value, bound, seen, free, env, fn_decls, fn_locals),
3883
+ ast::Expr::Catch(c) => {
3884
+ fvCollectRefsExpr(&c.value, bound, seen, free, env, fn_decls, fn_locals);
3885
+ match &c.handler {
3886
+ ast::CatchHandler::Default(d) => fvCollectRefsExpr(d, bound, seen, free, env, fn_decls, fn_locals),
3887
+ ast::CatchHandler::Block { binder, body } => {
3888
+ let mut inner_bound = bound.clone();
3889
+ inner_bound.insert(binder.clone());
3890
+ fvCollectBoundBlock(body, &mut inner_bound);
3891
+ fvCollectRefsBlock(body, &inner_bound, seen, free, env, fn_decls, fn_locals);
3892
+ }
3893
+ }
3894
+ }
3736
3895
  ast::Expr::Unary(u) => fvCollectRefsExpr(&u.operand, bound, seen, free, env, fn_decls, fn_locals),
3737
3896
  ast::Expr::Paren(inner) => fvCollectRefsExpr(inner, bound, seen, free, env, fn_decls, fn_locals),
3738
3897
  ast::Expr::Ternary(t) => {
@@ -3991,8 +4150,42 @@ impl<'a> Collector<'a> {
3991
4150
  self.nested_class_scratch_types.push((from, String::new()));
3992
4151
  }
3993
4152
  }
4153
+ for k in [plum_checker::ERR_FROM_NULLABLE, plum_checker::ERR_TO_NULLABLE] {
4154
+ if let Some(exp) = self.types.get(&(ptr + k)) {
4155
+ let ty = if k == plum_checker::ERR_FROM_NULLABLE { self.ty(expr) } else { self.ty(expr) };
4156
+ let _ = exp;
4157
+ let slot_ty = if k == plum_checker::ERR_FROM_NULLABLE {
4158
+ // the nullable ref is parked in a ref local
4159
+ match plum_checker::nullableTryPayload(&ty) {
4160
+ Some(p) => p.clone(),
4161
+ None => ty,
4162
+ }
4163
+ } else {
4164
+ ty
4165
+ };
4166
+ self.bind(&errScratchKey(expr, k), slot_ty);
4167
+ }
4168
+ }
3994
4169
  match expr {
3995
- ast::Expr::Yield(_) | ast::Expr::Bang(_) | ast::Expr::Catch(_) | ast::Expr::Fail(_) => {}
4170
+ ast::Expr::Yield(_) | ast::Expr::Fail(_) => {}
4171
+ ast::Expr::Bang(b) => {
4172
+ let ty = self.ty(&b.value);
4173
+ self.bind(&errScratch(expr), ty);
4174
+ self.walkExpr(&b.value);
4175
+ }
4176
+ ast::Expr::Catch(c) => {
4177
+ let ty = self.ty(&c.value);
4178
+ self.bind(&errScratch(expr), ty);
4179
+ self.walkExpr(&c.value);
4180
+ match &c.handler {
4181
+ ast::CatchHandler::Default(d) => self.walkExpr(d),
4182
+ ast::CatchHandler::Block { binder, body } => {
4183
+ let names = errSite(expr).map(|s| s.names).unwrap_or_default();
4184
+ self.bind(binder, PlumType::TTags(names.into_iter().map(|n| (n, None)).collect()));
4185
+ self.walkBlock(body);
4186
+ }
4187
+ }
4188
+ }
3996
4189
  ast::Expr::ClassCall(call) => {
3997
4190
  for fa in &call.fields {
3998
4191
  self.walkExpr(&fa.value);
@@ -4024,6 +4217,9 @@ impl<'a> Collector<'a> {
4024
4217
  ast::Expr::Try(inner) => {
4025
4218
  let key = expr as *const ast::Expr as usize;
4026
4219
  let ty = self.ty(inner);
4220
+ if errSite(expr).is_some() {
4221
+ self.bind(&errScratch(expr), ty.clone());
4222
+ }
4027
4223
  let slot = self.next_nested_class_slot;
4028
4224
  self.next_nested_class_slot += 1;
4029
4225
  self.nested_class_scratch.insert(key, slot);
@@ -4059,6 +4255,15 @@ impl<'a> Collector<'a> {
4059
4255
  ast::Expr::Attribute(a) => {
4060
4256
  self.walkExpr(&a.object);
4061
4257
  if let ast::AttrKind::Method(call) = &a.attr {
4258
+ if matches!(self.ty(&a.object), PlumType::TTags(_)) && call.name == "name" {
4259
+ self.bind(&errScratch(expr), PlumType::TBool);
4260
+ }
4261
+ // `a?.b` on an error union is inlined (no closure is built).
4262
+ if let (PlumType::TErr(inner, _), Some(cl)) = (self.ty(&a.object), andThenClosure(a, call)) {
4263
+ self.bind(&errScratch(expr), self.ty(&a.object));
4264
+ self.bind(&cl.params[0], (*inner).clone());
4265
+ self.walkBlock(&cl.body);
4266
+ }
4062
4267
  // A call through an fn-typed field needs a local to hold the
4063
4268
  // closure (read twice: env, then table index). Reserved even if
4064
4269
  // a same-named method wins at compile time (then left unused).
@@ -4283,6 +4488,14 @@ fn compileFnBodyImpl(
4283
4488
  ast::FnBody::Expr(e) => {
4284
4489
  compileExpr(e, &mut body, &local_ctx, state)?;
4285
4490
  }
4491
+ ast::FnBody::Block(block)
4492
+ if matches!(&ret_decl, PlumType::TErr(t, _) if matches!(**t, PlumType::TUnit))
4493
+ && !matches!(block.stmts.last(),
4494
+ Some(ast::Stmt::Expr(e, ..)) if matches!(inferLocalType(e, &local_ctx), PlumType::TErr(..))) =>
4495
+ {
4496
+ compileBlock(block, &mut body, &local_ctx, state)?;
4497
+ Instruction::I32Const(0).encode(&mut body);
4498
+ }
4286
4499
  ast::FnBody::Block(block) => {
4287
4500
  compileBlockAsFnBody(block, &mut body, &local_ctx, state, result_vt)?;
4288
4501
  }
@@ -4678,6 +4891,9 @@ fn compileStmt(
4678
4891
  Instruction::Return.encode(body);
4679
4892
  }
4680
4893
  ast::Stmt::Return(None) => {
4894
+ if matches!(&ctx.ret_type, Some(PlumType::TErr(t, _)) if matches!(**t, PlumType::TUnit)) {
4895
+ Instruction::I32Const(0).encode(body);
4896
+ }
4681
4897
  Instruction::Return.encode(body);
4682
4898
  }
4683
4899
  ast::Stmt::If(if_) => {
@@ -4751,6 +4967,11 @@ fn compileStmt(
4751
4967
  /// Returns true if the expression leaves a value on the wasm stack.
4752
4968
  fn exprHasResult(expr: &ast::Expr, ctx: &LocalCtx) -> bool {
4753
4969
  match expr {
4970
+ ast::Expr::Try(_) | ast::Expr::Bang(_) | ast::Expr::Catch(_)
4971
+ if matches!(inferLocalType(expr, ctx), PlumType::TUnit) =>
4972
+ {
4973
+ false
4974
+ }
4754
4975
  ast::Expr::FnCall(call) => {
4755
4976
  if ctx.locals.contains_key(&call.name)
4756
4977
  && matches!(
@@ -6126,6 +6347,44 @@ fn compileExpr(
6126
6347
  if SKIP_COERCION.with(|c| c.replace(0)) == ptr {
6127
6348
  return compileExprCore(expr, body, ctx, state);
6128
6349
  }
6350
+ if ctx.types.contains_key(&(ptr + plum_checker::ERR_WRAP)) {
6351
+ compileExprCore(expr, body, ctx, state)?;
6352
+ Instruction::I32Const(0).encode(body);
6353
+ return Ok(());
6354
+ }
6355
+ if ctx.types.contains_key(&(ptr + plum_checker::ERR_FROM_NULLABLE)) {
6356
+ let scratch = *ctx
6357
+ .locals
6358
+ .get(&errScratchKey(expr, plum_checker::ERR_FROM_NULLABLE))
6359
+ .ok_or_else(|| "internal codegen error: missing nullable->!T scratch".to_string())?;
6360
+ compileExprCore(expr, body, ctx, state)?;
6361
+ Instruction::LocalSet(scratch).encode(body);
6362
+ Instruction::LocalGet(scratch).encode(body);
6363
+ Instruction::LocalGet(scratch).encode(body);
6364
+ Instruction::RefIsNull.encode(body);
6365
+ Instruction::If(BlockType::Result(ValType::I32)).encode(body);
6366
+ Instruction::I32Const(errCode("Missing")?).encode(body);
6367
+ Instruction::Else.encode(body);
6368
+ Instruction::I32Const(0).encode(body);
6369
+ Instruction::End.encode(body);
6370
+ return Ok(());
6371
+ }
6372
+ if let Some(to) = ctx.types.get(&(ptr + plum_checker::ERR_TO_NULLABLE)) {
6373
+ let scratch = *ctx
6374
+ .locals
6375
+ .get(&errScratchKey(expr, plum_checker::ERR_TO_NULLABLE))
6376
+ .ok_or_else(|| "internal codegen error: missing !T->nullable scratch".to_string())?;
6377
+ let ty = inferLocalType(expr, ctx);
6378
+ compileExprCore(expr, body, ctx, state)?;
6379
+ emitSetLocals(scratch, &ty, body);
6380
+ Instruction::LocalGet(scratch + leafCount(&ty) - 1).encode(body);
6381
+ Instruction::If(BlockType::Result(plumTypeToValtype(to))).encode(body);
6382
+ Instruction::RefNull(nullableHeapType(to)).encode(body);
6383
+ Instruction::Else.encode(body);
6384
+ Instruction::LocalGet(scratch).encode(body);
6385
+ Instruction::End.encode(body);
6386
+ return Ok(());
6387
+ }
6129
6388
  if let Some(ety) = ctx.types.get(&(ptr + plum_checker::TRY_WRAP_OK)) {
6130
6389
  return compileTryWrap(expr, ety, true, body, ctx, state);
6131
6390
  }
@@ -6199,6 +6458,296 @@ fn emitMissingReturn(body: &mut Vec<u8>, ctx: &LocalCtx) -> Result<(), String> {
6199
6458
  Ok(())
6200
6459
  }
6201
6460
 
6461
+ /// A compiled error-union operand parked in its scratch local: `!T` as
6462
+ /// `(T leaves..., code)`, or a nullable `Try` as its (nullable) reference.
6463
+ struct ErrOperand {
6464
+ base: u32,
6465
+ ty: PlumType,
6466
+ success: PlumType,
6467
+ nullable: bool,
6468
+ }
6469
+
6470
+ impl ErrOperand {
6471
+ fn pushIsErr(&self, body: &mut Vec<u8>) {
6472
+ if self.nullable {
6473
+ Instruction::LocalGet(self.base).encode(body);
6474
+ Instruction::RefIsNull.encode(body);
6475
+ } else {
6476
+ self.pushCode(body).unwrap();
6477
+ Instruction::I32Const(0).encode(body);
6478
+ Instruction::I32Ne.encode(body);
6479
+ }
6480
+ }
6481
+ fn pushCode(&self, body: &mut Vec<u8>) -> Result<(), String> {
6482
+ if self.nullable {
6483
+ Instruction::I32Const(errCode("Missing")?).encode(body);
6484
+ } else {
6485
+ Instruction::LocalGet(self.base + leafCount(&self.ty) - 1).encode(body);
6486
+ }
6487
+ Ok(())
6488
+ }
6489
+ fn pushValue(&self, body: &mut Vec<u8>) {
6490
+ if self.nullable {
6491
+ Instruction::LocalGet(self.base).encode(body);
6492
+ } else {
6493
+ for i in 0..leafCount(&self.ty) - 1 {
6494
+ Instruction::LocalGet(self.base + i).encode(body);
6495
+ }
6496
+ }
6497
+ }
6498
+ }
6499
+
6500
+ /// Compiles `operand` (an error union) into `site_expr`'s scratch local.
6501
+ fn errOperand(
6502
+ site_expr: &ast::Expr,
6503
+ operand: &ast::Expr,
6504
+ body: &mut Vec<u8>,
6505
+ ctx: &LocalCtx,
6506
+ state: &mut ModuleState,
6507
+ ) -> Result<ErrOperand, String> {
6508
+ let ty = inferLocalType(operand, ctx);
6509
+ let base = *ctx
6510
+ .locals
6511
+ .get(&errScratch(site_expr))
6512
+ .ok_or_else(|| "internal codegen error: missing error-union scratch local".to_string())?;
6513
+ compileExpr(operand, body, ctx, state)?;
6514
+ if let PlumType::TErr(t, _) = &ty {
6515
+ emitSetLocals(base, &ty, body);
6516
+ let success = (**t).clone();
6517
+ return Ok(ErrOperand { base, ty, success, nullable: false });
6518
+ }
6519
+ if let Some(p) = plum_checker::nullableTryPayload(&ty) {
6520
+ Instruction::LocalSet(base).encode(body);
6521
+ let success = p.clone();
6522
+ return Ok(ErrOperand { base, ty, success, nullable: true });
6523
+ }
6524
+ Err(format!("codegen: '?', '!' and 'catch' on {} are not supported", ty))
6525
+ }
6526
+
6527
+ fn resultBlock(success: &PlumType) -> BlockType {
6528
+ if matches!(success, PlumType::TUnit) {
6529
+ BlockType::Empty
6530
+ } else {
6531
+ blockTypeFor(Some(plumTypeToValtype(success)))
6532
+ }
6533
+ }
6534
+
6535
+ /// `return (zeros, code)` for an enclosing `!T` fn, or null for a nullable-`Try` fn.
6536
+ fn emitErrReturn(
6537
+ push_code: &dyn Fn(&mut Vec<u8>) -> Result<(), String>,
6538
+ body: &mut Vec<u8>,
6539
+ ctx: &LocalCtx,
6540
+ ) -> Result<(), String> {
6541
+ let rt = ctx
6542
+ .ret_type
6543
+ .as_ref()
6544
+ .ok_or_else(|| "codegen: `fail`/`?` outside a function returning an error union".to_string())?;
6545
+ match rt {
6546
+ PlumType::TErr(t, _) => {
6547
+ if !matches!(**t, PlumType::TUnit) {
6548
+ emitZeros(t, body);
6549
+ }
6550
+ push_code(body)?;
6551
+ }
6552
+ _ if plum_checker::nullableTryPayload(rt).is_some() => {
6553
+ Instruction::RefNull(nullableHeapType(rt)).encode(body);
6554
+ }
6555
+ _ => return Err(format!("codegen: `fail`/`?` in a function returning {}", rt)),
6556
+ }
6557
+ Instruction::Return.encode(body);
6558
+ Ok(())
6559
+ }
6560
+
6561
+ fn calleeText(e: &ast::Expr) -> String {
6562
+ match e {
6563
+ ast::Expr::FnCall(c) => c.name.clone(),
6564
+ ast::Expr::Attribute(a) => match &a.attr {
6565
+ ast::AttrKind::Method(c) => c.name.clone(),
6566
+ ast::AttrKind::Field(f) => f.clone(),
6567
+ },
6568
+ ast::Expr::Var(n) => n.clone(),
6569
+ ast::Expr::Paren(i) => calleeText(i),
6570
+ _ => "value".to_string(),
6571
+ }
6572
+ }
6573
+
6574
+ fn compileErrTry(
6575
+ expr: &ast::Expr,
6576
+ inner: &ast::Expr,
6577
+ body: &mut Vec<u8>,
6578
+ ctx: &LocalCtx,
6579
+ state: &mut ModuleState,
6580
+ ) -> Result<(), String> {
6581
+ let op = errOperand(expr, inner, body, ctx, state)?;
6582
+ op.pushIsErr(body);
6583
+ Instruction::If(BlockType::Empty).encode(body);
6584
+ emitErrReturn(&|b| op.pushCode(b), body, ctx)?;
6585
+ Instruction::End.encode(body);
6586
+ op.pushValue(body);
6587
+ Ok(())
6588
+ }
6589
+
6590
+ fn compileErrBang(
6591
+ expr: &ast::Expr,
6592
+ b: &ast::BangExpr,
6593
+ body: &mut Vec<u8>,
6594
+ ctx: &LocalCtx,
6595
+ state: &mut ModuleState,
6596
+ ) -> Result<(), String> {
6597
+ let op = errOperand(expr, &b.value, body, ctx, state)?;
6598
+ let crash_idx = *ctx
6599
+ .func_ids
6600
+ .get(CRASH_FN_KEY)
6601
+ .ok_or_else(|| "internal codegen error: crash import missing".to_string())?;
6602
+ let callee = calleeText(&b.value);
6603
+ let file = ERR_INFO.with(|e| e.borrow().file.clone());
6604
+ let names = errSite(expr).map(|s| s.names).unwrap_or_default();
6605
+ let msg = |n: &str| format!("{} failed with {} at {}:{}:{}", callee, n, file, b.line, b.column);
6606
+ op.pushIsErr(body);
6607
+ Instruction::If(BlockType::Empty).encode(body);
6608
+ for n in &names {
6609
+ op.pushCode(body)?;
6610
+ Instruction::I32Const(errCode(n)?).encode(body);
6611
+ Instruction::I32Eq.encode(body);
6612
+ Instruction::If(BlockType::Empty).encode(body);
6613
+ compileStaticString(&msg(n), body, state);
6614
+ Instruction::Call(crash_idx).encode(body);
6615
+ Instruction::Unreachable.encode(body);
6616
+ Instruction::End.encode(body);
6617
+ }
6618
+ compileStaticString(&msg("error"), body, state);
6619
+ Instruction::Call(crash_idx).encode(body);
6620
+ Instruction::Unreachable.encode(body);
6621
+ Instruction::End.encode(body);
6622
+ op.pushValue(body);
6623
+ Ok(())
6624
+ }
6625
+
6626
+ fn compileErrCatch(
6627
+ expr: &ast::Expr,
6628
+ c: &ast::CatchExpr,
6629
+ body: &mut Vec<u8>,
6630
+ ctx: &LocalCtx,
6631
+ state: &mut ModuleState,
6632
+ ) -> Result<(), String> {
6633
+ let op = errOperand(expr, &c.value, body, ctx, state)?;
6634
+ op.pushIsErr(body);
6635
+ Instruction::If(resultBlock(&op.success)).encode(body);
6636
+ match &c.handler {
6637
+ ast::CatchHandler::Default(d) => {
6638
+ ctx.loop_extra_depth.set(ctx.loop_extra_depth.get() + 1);
6639
+ let r = compileExprInto(d, &op.success, body, ctx, state);
6640
+ ctx.loop_extra_depth.set(ctx.loop_extra_depth.get() - 1);
6641
+ r?;
6642
+ }
6643
+ ast::CatchHandler::Block { binder, body: blk } => {
6644
+ let names = errSite(expr).map(|s| s.names).unwrap_or_default();
6645
+ // `e` is a tag value of the set's names: build it from the code.
6646
+ for n in &names {
6647
+ op.pushCode(body)?;
6648
+ Instruction::I32Const(errCode(n)?).encode(body);
6649
+ Instruction::I32Eq.encode(body);
6650
+ Instruction::If(BlockType::Result(ValType::I32)).encode(body);
6651
+ Instruction::I32Const(tagId(n)).encode(body);
6652
+ Instruction::Else.encode(body);
6653
+ }
6654
+ Instruction::I32Const(0).encode(body);
6655
+ for _ in &names {
6656
+ Instruction::End.encode(body);
6657
+ }
6658
+ Instruction::StructNew(ctx.gc_types.tag_type_idx).encode(body);
6659
+ let idx = *ctx
6660
+ .locals
6661
+ .get(binder)
6662
+ .ok_or_else(|| format!("undeclared catch binder '{}'", binder))?;
6663
+ Instruction::LocalSet(idx).encode(body);
6664
+ let tags = PlumType::TTags(names.iter().map(|n| (n.clone(), None)).collect());
6665
+ ctx.type_env.borrow_mut().insert(binder.clone(), TypeScheme::mono(tags));
6666
+ let rvt = if matches!(op.success, PlumType::TUnit) { None } else { Some(plumTypeToValtype(&op.success)) };
6667
+ compileCaseBody(blk, rvt, body, ctx, state)?;
6668
+ }
6669
+ }
6670
+ Instruction::Else.encode(body);
6671
+ op.pushValue(body);
6672
+ Instruction::End.encode(body);
6673
+ Ok(())
6674
+ }
6675
+
6676
+ /// `a?.b` on an error union: `a.andThen(|x| x.b)` inlined.
6677
+ fn compileErrAndThen(
6678
+ expr: &ast::Expr,
6679
+ obj: &ast::Expr,
6680
+ cl: &ast::Closure,
6681
+ body: &mut Vec<u8>,
6682
+ ctx: &LocalCtx,
6683
+ state: &mut ModuleState,
6684
+ ) -> Result<(), String> {
6685
+ let op = errOperand(expr, obj, body, ctx, state)?;
6686
+ let out_ty = inferLocalType(expr, ctx);
6687
+ let PlumType::TErr(out_inner, _) = &out_ty else {
6688
+ return Err(format!("codegen: '?.' result is {}, not an error union", out_ty));
6689
+ };
6690
+ op.pushIsErr(body);
6691
+ Instruction::If(blockTypeFor(Some(plumTypeToValtype(&out_ty)))).encode(body);
6692
+ emitZeros(out_inner, body);
6693
+ op.pushCode(body)?;
6694
+ Instruction::Else.encode(body);
6695
+ let pidx = *ctx.locals.get(&cl.params[0]).ok_or_else(|| "missing '?.' binding".to_string())?;
6696
+ op.pushValue(body);
6697
+ emitSetLocals(pidx, &op.success, body);
6698
+ let (last, rest) = cl
6699
+ .body
6700
+ .stmts
6701
+ .split_last()
6702
+ .ok_or_else(|| "codegen: empty '?.' body".to_string())?;
6703
+ ctx.loop_extra_depth.set(ctx.loop_extra_depth.get() + 1);
6704
+ for st in rest {
6705
+ compileStmt(st, body, ctx, state)?;
6706
+ }
6707
+ let ast::Stmt::Expr(e, ..) = last else {
6708
+ return Err("codegen: '?.' body must be an expression".to_string());
6709
+ };
6710
+ compileExpr(e, body, ctx, state)?;
6711
+ ctx.loop_extra_depth.set(ctx.loop_extra_depth.get() - 1);
6712
+ if !matches!(inferLocalType(e, ctx), PlumType::TErr(..)) {
6713
+ Instruction::I32Const(0).encode(body);
6714
+ }
6715
+ Instruction::End.encode(body);
6716
+ Ok(())
6717
+ }
6718
+
6719
+ /// `e.name()` on a tag/error value: its text.
6720
+ fn compileTagName(
6721
+ expr: &ast::Expr,
6722
+ obj: &ast::Expr,
6723
+ tags: &std::collections::BTreeMap<String, Option<Box<PlumType>>>,
6724
+ body: &mut Vec<u8>,
6725
+ ctx: &LocalCtx,
6726
+ state: &mut ModuleState,
6727
+ ) -> Result<(), String> {
6728
+ let tmp = *ctx
6729
+ .locals
6730
+ .get(&errScratch(expr))
6731
+ .ok_or_else(|| "internal codegen error: missing name() scratch".to_string())?;
6732
+ compileExpr(obj, body, ctx, state)?;
6733
+ Instruction::StructGet { struct_type_index: ctx.gc_types.tag_type_idx, field_index: 0 }.encode(body);
6734
+ Instruction::LocalSet(tmp).encode(body);
6735
+ let svt = plumTypeToValtype(&PlumType::TStr);
6736
+ for n in tags.keys() {
6737
+ Instruction::LocalGet(tmp).encode(body);
6738
+ Instruction::I32Const(tagId(n)).encode(body);
6739
+ Instruction::I32Eq.encode(body);
6740
+ Instruction::If(BlockType::Result(svt)).encode(body);
6741
+ compileStaticString(n, body, state);
6742
+ Instruction::Else.encode(body);
6743
+ }
6744
+ compileStaticString("", body, state);
6745
+ for _ in tags {
6746
+ Instruction::End.encode(body);
6747
+ }
6748
+ Ok(())
6749
+ }
6750
+
6202
6751
  /// Converts a nullable `Try` value (null = `Missing`) into the ordinary struct
6203
6752
  /// layout of the wider `to` type (`Try[T, [Missing, ..]]`).
6204
6753
  fn compileNullableRelayout(
@@ -6241,8 +6790,23 @@ fn compileExprCore(
6241
6790
  ) -> Result<(), String> {
6242
6791
  match expr {
6243
6792
  ast::Expr::Yield(_) => return Err("internal codegen error: yield survived expansion".to_string()),
6244
- ast::Expr::Bang(_) | ast::Expr::Catch(_) | ast::Expr::Fail(_) => {
6793
+ ast::Expr::Bang(b) => compileErrBang(expr, b, body, ctx, state)?,
6794
+ ast::Expr::Catch(c) => compileErrCatch(expr, c, body, ctx, state)?,
6795
+ ast::Expr::Fail(f) => {
6796
+ let tag = f.tag.clone();
6797
+ let rt = ctx.ret_type.clone();
6798
+ match rt {
6799
+ Some(ref r @ PlumType::TApp(..)) if plum_checker::nullableTryPayload(r).is_none() => {
6800
+ let idx = ctx.gc_types.variantIdx(r, "Err").ok_or_else(|| {
6245
- return Err("`!`, `catch` and `fail` are not supported by codegen yet".to_string())
6801
+ format!("codegen: `fail` in a function returning {}", r)
6802
+ })?;
6803
+ Instruction::I32Const(tagId(&tag)).encode(body);
6804
+ Instruction::StructNew(ctx.gc_types.tag_type_idx).encode(body);
6805
+ Instruction::StructNew(idx).encode(body);
6806
+ Instruction::Return.encode(body);
6807
+ }
6808
+ _ => emitErrReturn(&|b| { Instruction::I32Const(errCode(&tag)?).encode(b); Ok(()) }, body, ctx)?,
6809
+ }
6246
6810
  }
6247
6811
  ast::Expr::Int(n) => {
6248
6812
  Instruction::I64Const(*n).encode(body);
@@ -6283,6 +6847,7 @@ fn compileExprCore(
6283
6847
  // `else` branch — the same stack-polymorphism-after-`return` rule
6284
6848
  // `compileStmt`'s own `Return` arm relies on, just reached from
6285
6849
  // inside an expression here for the first time.
6850
+ ast::Expr::Try(inner) if errSite(expr).is_some() => compileErrTry(expr, inner, body, ctx, state)?,
6286
6851
  ast::Expr::Try(inner) => {
6287
6852
  let inner_ty = inferLocalType(inner, ctx);
6288
6853
  let enum_name = match plum_checker::nominalName(&inner_ty) {
@@ -7024,7 +7589,13 @@ fn compileExprCore(
7024
7589
  compileFieldRead(&attr.object, field_name, &obj_ty, expr, body, ctx, state)?
7025
7590
  }
7026
7591
  ast::AttrKind::Method(call) => {
7592
+ if let (PlumType::TErr(..), Some(cl)) = (&obj_ty, andThenClosure(attr, call)) {
7593
+ compileErrAndThen(expr, &attr.object, cl, body, ctx, state)?
7594
+ } else if let (PlumType::TTags(tags), "name", true) = (&obj_ty, call.name.as_str(), call.args.is_empty()) {
7595
+ compileTagName(expr, &attr.object, tags, body, ctx, state)?
7596
+ } else {
7027
- compileMethodCall(&attr.object, call, &obj_ty, expr, false, body, ctx, state)?
7597
+ compileMethodCall(&attr.object, call, &obj_ty, expr, false, body, ctx, state)?
7598
+ }
7028
7599
  }
7029
7600
  }
7030
7601
  }
plum-wasm-codegen/tests/codegen_tests.rs CHANGED
@@ -2812,3 +2812,331 @@ fn extend_replaces_dummy_enums() {
2812
2812
  assert!(!text.contains(&format!("enum {f} =")), "{f} still has a dummy enum");
2813
2813
  }
2814
2814
  }
2815
+
2816
+ // ---- Zig-style error unions (E3) ----
2817
+
2818
+ fn compileUnmerged(src: &str) -> Vec<u8> {
2819
+ assertValid(src)
2820
+ }
2821
+
2822
+ fn runInt(src: &str) -> i64 {
2823
+ let bytes = compileUnmerged(src);
2824
+ let engine = gcEngine();
2825
+ let module = wasmtime::Module::new(&engine, &bytes).unwrap();
2826
+ let mut store = wasmtime::Store::new(&engine, ());
2827
+ let instance = wasmtime::Instance::new(&mut store, &module, &[]).unwrap();
2828
+ instance.get_typed_func::<(), i64>(&mut store, "main").unwrap().call(&mut store, ()).unwrap()
2829
+ }
2830
+
2831
+ /// Runs a std-importing program; returns (result or trap, crash messages seen).
2832
+ fn runWithCrash(name: &str, src: &str) -> (Result<i64, String>, Vec<String>) {
2833
+ let dir = std::env::temp_dir().join(format!("plum_e3_{}_{}", name, std::process::id()));
2834
+ std::fs::create_dir_all(&dir).unwrap();
2835
+ let path = dir.join("main.plum");
2836
+ std::fs::write(&path, src).unwrap();
2837
+ let source = plum_core::loadAndMerge(&path, &repoRoot()).expect("load");
2838
+ plum_checker::checkSource(&source).unwrap_or_else(|e| {
2839
+ panic!("check errors: {:?}", e.iter().map(|x| x.message.clone()).collect::<Vec<_>>())
2840
+ });
2841
+ let bytes = compileSource(&source).expect("compile failed");
2842
+ wasmparser::validate(&bytes).expect("valid wasm");
2843
+ let _ = std::fs::remove_dir_all(&dir);
2844
+ let engine = gcEngine();
2845
+ let module = wasmtime::Module::new(&engine, &bytes).unwrap();
2846
+ let mut store = wasmtime::Store::new(&engine, Vec::<String>::new());
2847
+ let mut linker: wasmtime::Linker<Vec<String>> = wasmtime::Linker::new(&engine);
2848
+ for imp in module.imports() {
2849
+ if imp.module() == "plum" && imp.name() == "crash" {
2850
+ let ty = imp.ty().func().cloned().unwrap();
2851
+ linker
2852
+ .func_new("plum", "crash", ty, |mut caller, params, _| {
2853
+ let wasmtime::Val::AnyRef(Some(s)) = &params[0] else { panic!("Str expected") };
2854
+ let st = s.unwrap_struct(&caller).unwrap();
2855
+ let wasmtime::Val::AnyRef(Some(buf)) = st.field(&mut caller, 0).unwrap() else { panic!() };
2856
+ let bs = buf.unwrap_struct(&caller).unwrap();
2857
+ let wasmtime::Val::AnyRef(Some(arr)) = bs.field(&mut caller, 0).unwrap() else { panic!() };
2858
+ let wasmtime::Val::I64(len) = bs.field(&mut caller, 1).unwrap() else { panic!() };
2859
+ let arr = arr.unwrap_array(&caller).unwrap();
2860
+ let mut b = vec![0u8; arr.len(&caller).unwrap() as usize];
2861
+ arr.copy_to_i8_slice(&mut caller, &mut b).unwrap();
2862
+ b.truncate(len as usize);
2863
+ caller.data_mut().push(String::from_utf8_lossy(&b).into_owned());
2864
+ Ok(())
2865
+ })
2866
+ .unwrap();
2867
+ }
2868
+ }
2869
+ linker.define_unknown_imports_as_default_values(&mut store, &module).unwrap();
2870
+ let instance = linker.instantiate(&mut store, &module).unwrap();
2871
+ let main = instance.get_typed_func::<(), i64>(&mut store, "main").unwrap();
2872
+ let r = main.call(&mut store, ()).map_err(|e| e.to_string());
2873
+ (r, store.data().clone())
2874
+ }
2875
+
2876
+ #[test]
2877
+ fn error_union_multi_value() {
2878
+ let bytes = assertValid("fun f(n: Int) -> !Int =\n if n < 0\n fail Neg\n n\n\nfun main() -> Int =\n f(3) catch 0\n");
2879
+ let mut found = false;
2880
+ for payload in wasmparser::Parser::new(0).parse_all(&bytes) {
2881
+ if let wasmparser::Payload::TypeSection(s) = payload.unwrap() {
2882
+ for g in s {
2883
+ for t in g.unwrap().types() {
2884
+ if let wasmparser::CompositeInnerType::Func(f) = &t.composite_type.inner {
2885
+ if f.params() == [wasmparser::ValType::I64]
2886
+ && f.results() == [wasmparser::ValType::I64, wasmparser::ValType::I32]
2887
+ {
2888
+ found = true;
2889
+ }
2890
+ }
2891
+ }
2892
+ }
2893
+ }
2894
+ }
2895
+ assert!(found, "no (i64)->(i64,i32) function type");
2896
+ }
2897
+
2898
+ #[test]
2899
+ fn fail_and_question_propagate() {
2900
+ let src = "\
2901
+ fun a(n: Int) -> !Int =
2902
+ if n < 0
2903
+ fail Negative
2904
+ n * 2
2905
+
2906
+ fun b(n: Int) -> !Int =
2907
+ x := a(n)?
2908
+ x + 1
2909
+
2910
+ fun main() -> Int =
2911
+ p := b(5) catch 100
2912
+ q := b(-5) catch 200
2913
+ p * 1000 + q
2914
+ ";
2915
+ assert_eq!(runInt(src), 11 * 1000 + 200);
2916
+ }
2917
+
2918
+ #[test]
2919
+ fn catch_default_runtime() {
2920
+ let src = "\
2921
+ fun f(n: Int) -> !Int =
2922
+ if n == 0
2923
+ fail Zero
2924
+ n
2925
+
2926
+ fun main() -> Int =
2927
+ {f(7) catch 1} * 10 + {f(0) catch 3}
2928
+ ";
2929
+ assert_eq!(runInt(src), 73);
2930
+ }
2931
+
2932
+ #[test]
2933
+ fn catch_fail_maps_tag() {
2934
+ let src = "\
2935
+ fun inner(n: Int) -> !Int =
2936
+ if n == 0
2937
+ fail Zero
2938
+ n
2939
+
2940
+ fun outer(n: Int) -> !Int =
2941
+ x := inner(n) catch fail Mapped
2942
+ x + 1
2943
+
2944
+ fun main() -> Int =
2945
+ a := outer(0) catch e
2946
+ match e
2947
+ Mapped => 40
2948
+ b := outer(1) catch 99
2949
+ a + b
2950
+ ";
2951
+ assert_eq!(runInt(src), 42);
2952
+ }
2953
+
2954
+ #[test]
2955
+ fn catch_block_binds_error() {
2956
+ let src = "\
2957
+ fun f(n: Int) -> !Int =
2958
+ if n == 1
2959
+ fail One
2960
+ if n == 2
2961
+ fail Two
2962
+ n
2963
+
2964
+ fun main() -> Int =
2965
+ a := f(1) catch e
2966
+ match e
2967
+ One => 10
2968
+ Two => 20
2969
+ b := f(2) catch e
2970
+ match e
2971
+ One => 10
2972
+ Two => 20
2973
+ c := f(3) catch e
2974
+ return 0
2975
+ a * 100 + b * 10 + c
2976
+ ";
2977
+ assert_eq!(runInt(src), 10 * 100 + 20 * 10 + 3);
2978
+ }
2979
+
2980
+ #[test]
2981
+ fn e_name_runtime() {
2982
+ let src = "\
2983
+ import std/Str
2984
+ import std/Number
2985
+
2986
+ fun f() -> !Int =
2987
+ fail NotFound
2988
+
2989
+ fun main() -> Int =
2990
+ s := f() catch e
2991
+ e.name()
2992
+ s.length()
2993
+ ";
2994
+ // the catch block must yield Int, so name() is used for its length instead
2995
+ let src = src.replace(" s := f() catch e\n e.name()\n s.length()", " n := f() catch e\n e.name().length()\n n");
2996
+ let (r, _) = runWithCrash("ename", &src);
2997
+ assert_eq!(r.unwrap(), 8);
2998
+ }
2999
+
3000
+ #[test]
3001
+ fn bang_crashes_with_message() {
3002
+ let src = "\
3003
+ import std/Str
3004
+ import std/Number
3005
+
3006
+ fun risky() -> !Int =
3007
+ fail Boom
3008
+
3009
+ fun main() -> Int =
3010
+ risky()!
3011
+ ";
3012
+ let (r, msgs) = runWithCrash("bang", src);
3013
+ assert!(r.is_err());
3014
+ assert_eq!(msgs.len(), 1, "{msgs:?}");
3015
+ assert!(msgs[0].starts_with("risky failed with Boom at "), "{}", msgs[0]);
3016
+ assert!(msgs[0].ends_with(":8:3"), "{}", msgs[0]);
3017
+ let ok = "import std/Str\nimport std/Number\n\nfun f() -> !Int =\n 7\n\nfun main() -> Int =\n f()!\n";
3018
+ assert_eq!(runWithCrash("bangok", ok).0.unwrap(), 7);
3019
+ }
3020
+
3021
+ #[test]
3022
+ fn unit_error_union() {
3023
+ let src = "\
3024
+ fun check(n: Int) -> !Unit =
3025
+ if n < 0
3026
+ fail Neg
3027
+
3028
+ fun run(n: Int) -> !Int =
3029
+ check(n)?
3030
+ n
3031
+
3032
+ fun main() -> Int =
3033
+ {run(4) catch 0} + {run(-1) catch 5}
3034
+ ";
3035
+ assert_eq!(runInt(src), 9);
3036
+ }
3037
+
3038
+ #[test]
3039
+ fn error_union_boxed_in_list() {
3040
+ let src = "\
3041
+ import std/Str
3042
+ import std/Number
3043
+ import std/List
3044
+
3045
+ fun f(n: Int) -> !Int =
3046
+ if n < 0
3047
+ fail Neg
3048
+ n
3049
+
3050
+ fun main() -> Int =
3051
+ xs := List[!Int]()
3052
+ xs.push(f(4))
3053
+ xs.push(f(-1))
3054
+ a := xs.at(0) catch 0
3055
+ b := xs.at(1) catch 100
3056
+ a + b
3057
+ ";
3058
+ assert_eq!(runWithCrash("boxlist", src).0.unwrap(), 104);
3059
+ }
3060
+
3061
+ #[test]
3062
+ fn error_union_in_class_field() {
3063
+ let src = "\
3064
+ enum Box =
3065
+ | Box(v: !Int)
3066
+
3067
+ fun f(n: Int) -> !Int =
3068
+ if n < 0
3069
+ fail Neg
3070
+ n
3071
+
3072
+ fun main() -> Int =
3073
+ a := Box(v: f(6))
3074
+ b := Box(v: f(-6))
3075
+ {a.v catch 0} * 10 + {b.v catch 1}
3076
+ ";
3077
+ assert_eq!(runInt(src), 61);
3078
+ }
3079
+
3080
+ #[test]
3081
+ fn safe_nav_on_error_union() {
3082
+ let src = "\
3083
+ enum User =
3084
+ | User(age: Int)
3085
+
3086
+ fun load(n: Int) -> !User =
3087
+ if n < 0
3088
+ fail NoUser
3089
+ User(age: n)
3090
+
3091
+ fun age(n: Int) -> !Int =
3092
+ load(n)?.age
3093
+
3094
+ fun main() -> Int =
3095
+ {age(30) catch 0} + {age(-1) catch 1}
3096
+ ";
3097
+ assert_eq!(runInt(src), 31);
3098
+ }
3099
+
3100
+ #[test]
3101
+ fn nullable_ref_still_null() {
3102
+ let src = "\
3103
+ import std/Str
3104
+ import std/Number
3105
+ import std/Try
3106
+
3107
+ enum Item =
3108
+ | Item(v: Int)
3109
+
3110
+ fun find(n: Int) -> Item? =
3111
+ if n < 0
3112
+ fail Missing
3113
+ Item(v: n)
3114
+
3115
+ fun main() -> Int =
3116
+ a := find(3) catch Item(v: 0)
3117
+ b := find(-3) catch Item(v: 5)
3118
+ a.v * 10 + b.v
3119
+ ";
3120
+ assert_eq!(runWithCrash("nullable", src).0.unwrap(), 35);
3121
+ }
3122
+
3123
+ #[test]
3124
+ fn no_alloc_on_error_path() {
3125
+ let src = "\
3126
+ fun a(n: Int) -> !Int =
3127
+ if n < 0
3128
+ fail Neg
3129
+ n
3130
+
3131
+ fun b(n: Int) -> !Int =
3132
+ x := a(n)?
3133
+ y := a(x + 1)?
3134
+ x + y
3135
+
3136
+ fun main() -> Int =
3137
+ b(2) catch 0
3138
+ ";
3139
+ let bytes = assertValid(src);
3140
+ let ops = opsOf(&bytes, "b");
3141
+ assert_eq!(countOps(&ops, "StructNew"), 0, "{ops:?}");
3142
+ }