plum

#treesitter#compiler#wasm

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

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


3254688Peter John 2026-07-20T13:27:12+05:30
fix(plum-wasm-codegen): propagate a value through tail-position match/if without explicit return
plum-wasm-codegen/src/lib.rs CHANGED
@@ -255,6 +255,10 @@ fn plum_type_to_valtype(t: &PlumType) -> ValType {
255
255
  }
256
256
  }
257
257
 
258
+ fn block_type_for(result_vt: Option<ValType>) -> BlockType {
259
+ result_vt.map(BlockType::Result).unwrap_or(BlockType::Empty)
260
+ }
261
+
258
262
  fn ret_type_to_wasm(ret: Option<&ast::ReturnType>) -> Option<ValType> {
259
263
  ret.and_then(|r| ast_type_to_wasm(&r.name))
260
264
  }
@@ -617,14 +621,14 @@ fn compile_fn_body(f: &ast::Fn, ctx: &CompileCtx, state: &mut ModuleState) -> Re
617
621
  bump_global: ctx.bump_global,
618
622
  };
619
623
 
620
- let has_return_value = f.returns.as_ref().map(|r| r.name != "Unit").unwrap_or(false);
624
+ let result_vt = ret_type_to_wasm(f.returns.as_ref());
621
625
 
622
626
  match &f.body {
623
627
  ast::FnBody::Expr(e) => {
624
628
  compile_expr(e, &mut body, &local_ctx, state)?;
625
629
  }
626
630
  ast::FnBody::Block(block) => {
627
- compile_block_as_fn_body(block, &mut body, &local_ctx, state, has_return_value)?;
631
+ compile_block_as_fn_body(block, &mut body, &local_ctx, state, result_vt)?;
628
632
  }
629
633
  }
630
634
 
@@ -639,70 +643,119 @@ fn compile_block(block: &ast::Block, body: &mut Vec<u8>, ctx: &LocalCtx, state:
639
643
  Ok(())
640
644
  }
641
645
 
646
+ /// Compiles a case/branch body either as an ordinary statement block (`result_vt: None`)
647
+ /// or, when in value position, via `compile_block_in_value_position` so its own tail
648
+ /// statement propagates a value instead of being dropped.
649
+ fn compile_case_body(
650
+ block: &ast::Block,
651
+ result_vt: Option<ValType>,
652
+ body: &mut Vec<u8>,
653
+ ctx: &LocalCtx,
654
+ state: &mut ModuleState,
655
+ ) -> Result<(), String> {
656
+ match result_vt {
657
+ Some(vt) => compile_block_in_value_position(block, vt, body, ctx, state),
658
+ None => compile_block(block, body, ctx, state),
659
+ }
660
+ }
661
+
662
+ /// Compiles a block whose value must be produced when control reaches its end — every
663
+ /// statement except the last compiles normally; the last is compiled via
664
+ /// `compile_stmt_in_value_position`.
665
+ fn compile_block_in_value_position(
666
+ block: &ast::Block,
667
+ result_vt: ValType,
668
+ body: &mut Vec<u8>,
669
+ ctx: &LocalCtx,
670
+ state: &mut ModuleState,
671
+ ) -> Result<(), String> {
672
+ let (last, rest) = block.stmts.split_last().ok_or_else(|| {
673
+ "codegen: function has a control-flow path that doesn't produce a return value (empty branch)".to_string()
674
+ })?;
675
+ for stmt in rest {
676
+ compile_stmt(stmt, body, ctx, state)?;
677
+ }
678
+ compile_stmt_in_value_position(last, result_vt, body, ctx, state)
679
+ }
680
+
681
+ /// Compiles a single statement in value position: a bare expression is left on the stack
682
+ /// (not dropped); `return`/`todo` compile normally (both are stack-polymorphic in wasm —
642
- /// True if control can never fall through past this statement every reachable path
683
+ /// control never falls through past them, so no value is needed on this path); `if`/`match`
643
- /// ends in a `return`. Used to decide whether a tail-position If/Match needs a
684
+ /// recurse so every arm/branch resolves the same way. Any other statement kind can't
644
- /// trailing `unreachable` to satisfy wasm's per-block (not whole-function) validation
685
+ /// produce a value, so this returns a clear error instead of ever emitting wasm that
645
- /// when the function declares a non-Unit return type.
686
+ /// would fail validation.
687
+ fn compile_stmt_in_value_position(
688
+ stmt: &ast::Stmt,
689
+ result_vt: ValType,
690
+ body: &mut Vec<u8>,
691
+ ctx: &LocalCtx,
692
+ state: &mut ModuleState,
646
- fn stmt_always_diverges(stmt: &ast::Stmt) -> bool {
693
+ ) -> Result<(), String> {
647
694
  match stmt {
695
+ ast::Stmt::Expr(e) => compile_expr(e, body, ctx, state),
648
- ast::Stmt::Return(_) | ast::Stmt::Todo => true,
696
+ ast::Stmt::Return(_) | ast::Stmt::Todo => compile_stmt(stmt, body, ctx, state),
649
- ast::Stmt::If(if_) => {
650
- if_.else_.is_some()
651
- && block_always_diverges(&if_.body)
652
- && if_.else_ifs.iter().all(|ei| block_always_diverges(&ei.body))
653
- && if_.else_.as_ref().is_some_and(block_always_diverges)
697
+ ast::Stmt::If(if_) => compile_if(if_, Some(result_vt), body, ctx, state),
654
- }
655
- ast::Stmt::Match(m) => !m.cases.is_empty() && m.cases.iter().all(|c| block_always_diverges(&c.body)),
698
+ ast::Stmt::Match(m) => compile_match(m, body, ctx, state, Some(result_vt)),
656
- _ => false,
699
+ _ => Err(
700
+ "codegen: function has a control-flow path that doesn't produce a return value".to_string(),
701
+ ),
657
702
  }
658
703
  }
659
704
 
705
+ /// Compiles an `if`/`else if`/`else` chain. `result_vt` is `None` for an ordinary statement
706
+ /// (each branch is `BlockType::Empty`, nothing left on the stack) or `Some(vt)` when this
707
+ /// `if` is in value position — every branch must then leave a `vt` value on the stack, which
708
+ /// requires an `else` (a value can't be produced on a path that doesn't exist).
709
+ fn compile_if(
710
+ if_: &ast::If,
711
+ result_vt: Option<ValType>,
712
+ body: &mut Vec<u8>,
713
+ ctx: &LocalCtx,
714
+ state: &mut ModuleState,
715
+ ) -> Result<(), String> {
716
+ if result_vt.is_some() && if_.else_.is_none() {
717
+ return Err(
718
+ "codegen: function has a control-flow path that doesn't produce a return value (if without else)".to_string(),
719
+ );
720
+ }
721
+ let bt = block_type_for(result_vt);
722
+ compile_expr(&if_.condition, body, ctx, state)?;
723
+ Instruction::If(bt).encode(body);
724
+ compile_case_body(&if_.body, result_vt, body, ctx, state)?;
725
+ if !if_.else_ifs.is_empty() || if_.else_.is_some() {
726
+ Instruction::Else.encode(body);
727
+ for ei in &if_.else_ifs {
728
+ compile_expr(&ei.condition, body, ctx, state)?;
729
+ Instruction::If(bt).encode(body);
730
+ compile_case_body(&ei.body, result_vt, body, ctx, state)?;
731
+ Instruction::Else.encode(body);
732
+ }
733
+ if let Some(else_block) = &if_.else_ {
660
- fn block_always_diverges(block: &ast::Block) -> bool {
734
+ compile_case_body(else_block, result_vt, body, ctx, state)?;
735
+ }
661
- block.stmts.last().map(stmt_always_diverges).unwrap_or(false)
736
+ for _ in &if_.else_ifs {
737
+ Instruction::End.encode(body);
738
+ }
739
+ }
740
+ Instruction::End.encode(body);
741
+ Ok(())
662
742
  }
663
743
 
664
- /// Compiles a block that is the body of a function. If the function returns a value
744
+ /// Compiles a block that is the body of a function. If the function returns a value,
665
- /// and the last statement is an expression, that expression's value is left on the
745
+ /// its tail statement is compiled in value position (see `compile_stmt_in_value_position`)
746
+ /// so a bare expression, or an `if`/`match` whose arms resolve to one, propagates that
666
- /// stack instead of being dropped.
747
+ /// value instead of being dropped.
667
748
  fn compile_block_as_fn_body(
668
749
  block: &ast::Block,
669
750
  body: &mut Vec<u8>,
670
751
  ctx: &LocalCtx,
671
752
  state: &mut ModuleState,
672
- has_return_value: bool,
753
+ result_vt: Option<ValType>,
673
754
  ) -> Result<(), String> {
674
- let stmts = &block.stmts;
675
- if has_return_value {
676
- if let Some((last, rest)) = stmts.split_last() {
677
- for stmt in rest {
678
- compile_stmt(stmt, body, ctx, state)?;
679
- }
680
- match last {
755
+ match result_vt {
681
- ast::Stmt::Expr(e) => {
682
- compile_expr(e, body, ctx, state)?;
756
+ Some(vt) => compile_block_in_value_position(block, vt, body, ctx, state),
683
- // do NOT drop — this is the return value
684
- }
685
- _ if stmt_always_diverges(last) => {
686
- compile_stmt(last, body, ctx, state)?;
757
+ None => compile_block(block, body, ctx, state),
687
- // Every branch of this If/Match already `return`s; if it somehow
688
- // falls through anyway (a bug, or a non-exhaustive match), trap
689
- // rather than continue with the required result value missing.
690
- // This also satisfies wasm validation, which requires a value at
691
- // the function's `end` regardless of whether every branch inside
692
- // an (BlockType::Empty) if/else chain already returned.
693
- Instruction::Unreachable.encode(body);
694
- }
695
- _ => {
696
- compile_stmt(last, body, ctx, state)?;
697
- }
698
- }
699
- return Ok(());
700
- }
701
758
  }
702
- for stmt in stmts {
703
- compile_stmt(stmt, body, ctx, state)?;
704
- }
705
- Ok(())
706
759
  }
707
760
 
708
761
  fn compile_stmt(stmt: &ast::Stmt, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mut ModuleState) -> Result<(), String> {
@@ -728,25 +781,7 @@ fn compile_stmt(stmt: &ast::Stmt, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mu
728
781
  Instruction::Return.encode(body);
729
782
  }
730
783
  ast::Stmt::If(if_) => {
731
- compile_expr(&if_.condition, body, ctx, state)?;
732
- Instruction::If(BlockType::Empty).encode(body);
733
- compile_block(&if_.body, body, ctx, state)?;
734
- if !if_.else_ifs.is_empty() || if_.else_.is_some() {
735
- Instruction::Else.encode(body);
736
- for ei in &if_.else_ifs {
737
- compile_expr(&ei.condition, body, ctx, state)?;
738
- Instruction::If(BlockType::Empty).encode(body);
739
- compile_block(&ei.body, body, ctx, state)?;
740
- Instruction::Else.encode(body);
741
- }
742
- if let Some(else_block) = &if_.else_ {
743
- compile_block(else_block, body, ctx, state)?;
784
+ compile_if(if_, None, body, ctx, state)?;
744
- }
745
- for _ in &if_.else_ifs {
746
- Instruction::End.encode(body);
747
- }
748
- }
749
- Instruction::End.encode(body);
750
785
  }
751
786
  ast::Stmt::While(w) => {
752
787
  Instruction::Block(BlockType::Empty).encode(body);
@@ -805,7 +840,7 @@ fn compile_stmt(stmt: &ast::Stmt, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mu
805
840
  Instruction::Br(0).encode(body);
806
841
  }
807
842
  ast::Stmt::Match(m) => {
808
- compile_match(m, body, ctx, state)?;
843
+ compile_match(m, body, ctx, state, None)?;
809
844
  }
810
845
  ast::Stmt::Assert(e) => {
811
846
  compile_expr(e, body, ctx, state)?;
@@ -841,7 +876,60 @@ fn expr_has_result(expr: &ast::Expr, ctx: &LocalCtx) -> bool {
841
876
  }
842
877
  }
843
878
 
879
+ /// True if `cases` consists solely of enum-tag patterns (bare variant names or
880
+ /// constructor patterns, no wildcard/binding/int/etc.) that between them cover every
881
+ /// variant of a single enum type. When that holds, a match compiled in value position
882
+ /// can never actually fall through past the last arm at runtime — even though the
883
+ /// patterns don't include an explicit wildcard/binding catch-all — so the "ran out of
884
+ /// patterns" fallback in `compile_match_arms` is provably unreachable code, not a real
885
+ /// gap. `compile_match` uses this to append a synthetic trap-and-never-fall-through
886
+ /// wildcard arm (rather than let the arms recursion hit its non-exhaustive-match error)
887
+ /// so previously-working exhaustive enum matches (e.g. `Some`/`None`, `True`/`False`)
888
+ /// keep compiling even without a trailing wildcard, while a genuinely non-exhaustive
889
+ /// match (an `Int` match, or an enum match missing a variant) still gets a clear error.
844
- fn compile_match(m: &ast::Match, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mut ModuleState) -> Result<(), String> {
890
+ fn match_covers_every_enum_variant(cases: &[ast::Case], subject_vt: ValType, ctx: &LocalCtx) -> bool {
891
+ if subject_vt != ValType::I32 {
892
+ return false;
893
+ }
894
+ let mut enum_name: Option<&str> = None;
895
+ let mut tags_seen: std::collections::BTreeSet<i32> = std::collections::BTreeSet::new();
896
+ for case in cases {
897
+ let variant_name = match case.patterns.first() {
898
+ Some(ast::CasePattern::Name(n))
899
+ if n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) && ctx.enum_variants.contains_key(n) =>
900
+ {
901
+ n.as_str()
902
+ }
903
+ Some(ast::CasePattern::Class { name, .. }) => name.as_str(),
904
+ _ => return false,
905
+ };
906
+ let info = match ctx.enum_variants.get(variant_name) {
907
+ Some(info) => info,
908
+ None => return false,
909
+ };
910
+ match enum_name {
911
+ Some(en) if en != info.enum_name => return false,
912
+ Some(_) => {}
913
+ None => enum_name = Some(&info.enum_name),
914
+ }
915
+ tags_seen.insert(info.tag);
916
+ }
917
+ match enum_name {
918
+ Some(en) => {
919
+ let total_variants = ctx.enum_variants.values().filter(|v| v.enum_name == en).count();
920
+ !tags_seen.is_empty() && tags_seen.len() == total_variants
921
+ }
922
+ None => false,
923
+ }
924
+ }
925
+
926
+ fn compile_match(
927
+ m: &ast::Match,
928
+ body: &mut Vec<u8>,
929
+ ctx: &LocalCtx,
930
+ state: &mut ModuleState,
931
+ result_vt: Option<ValType>,
932
+ ) -> Result<(), String> {
845
933
  if m.subjects.len() != 1 {
846
934
  return Err("codegen: multi-subject match is not yet supported".to_string());
847
935
  }
@@ -859,32 +947,55 @@ fn compile_match(m: &ast::Match, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mut
859
947
  compile_expr(subject, body, ctx, state)?;
860
948
  Instruction::LocalSet(scratch_local).encode(body);
861
949
 
950
+ // A match in value position whose arms already cover every variant of the
951
+ // subject's enum type (by explicit tag/constructor patterns, no wildcard) is
952
+ // exhaustive at runtime even though `compile_match_arms` can't see that from the
953
+ // remaining-cases slice alone. Give it a synthetic trailing `_ => todo` arm so the
954
+ // "out of patterns" fallback compiles to an (unreachable, but valid) trap instead
955
+ // of a spurious non-exhaustive-match error.
956
+ if result_vt.is_some() && match_covers_every_enum_variant(&m.cases, subject_vt, ctx) {
957
+ let mut cases = m.cases.clone();
958
+ cases.push(ast::Case {
959
+ patterns: vec![ast::CasePattern::Wildcard],
960
+ body: ast::Block { stmts: vec![ast::Stmt::Todo] },
961
+ });
962
+ return compile_match_arms(&cases, subject_vt, scratch_local, result_vt, body, ctx, state);
963
+ }
964
+
862
- compile_match_arms(&m.cases, subject_vt, scratch_local, body, ctx, state)
965
+ compile_match_arms(&m.cases, subject_vt, scratch_local, result_vt, body, ctx, state)
863
966
  }
864
967
 
865
968
  fn compile_match_arms(
866
969
  cases: &[ast::Case],
867
970
  subject_vt: ValType,
868
971
  scratch_local: u32,
972
+ result_vt: Option<ValType>,
869
973
  body: &mut Vec<u8>,
870
974
  ctx: &LocalCtx,
871
975
  state: &mut ModuleState,
872
976
  ) -> Result<(), String> {
873
977
  let (case, rest) = match cases.split_first() {
978
+ None => {
979
+ return match result_vt {
980
+ Some(_) => Err(
981
+ "codegen: function has a control-flow path that doesn't produce a return value (non-exhaustive match)".to_string(),
982
+ ),
874
- None => return Ok(()),
983
+ None => Ok(()),
984
+ };
985
+ }
875
986
  Some(pair) => pair,
876
987
  };
877
988
  let pat = case.patterns.first().ok_or_else(|| "codegen: match case has no pattern".to_string())?;
878
989
  match pat {
879
990
  ast::CasePattern::Wildcard => {
880
991
  // Any cases after a wildcard are unreachable, matching real match semantics.
881
- compile_block(&case.body, body, ctx, state)
992
+ compile_case_body(&case.body, result_vt, body, ctx, state)
882
993
  }
883
994
  ast::CasePattern::Name(n) => {
884
995
  let is_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
885
996
  && ctx.enum_variants.contains_key(n);
886
997
  if is_variant {
887
- compile_variant_eq_arm(n, subject_vt, scratch_local, case, rest, body, ctx, state)
998
+ compile_variant_eq_arm(n, subject_vt, scratch_local, result_vt, case, rest, body, ctx, state)
888
999
  } else {
889
1000
  let idx = ctx
890
1001
  .locals
@@ -894,7 +1005,7 @@ fn compile_match_arms(
894
1005
  Instruction::LocalGet(scratch_local).encode(body);
895
1006
  Instruction::LocalSet(idx).encode(body);
896
1007
  ctx.type_env.borrow_mut().insert(n.clone(), TypeScheme::mono(plum_type_from_valtype_hint(subject_vt)));
897
- compile_block(&case.body, body, ctx, state)
1008
+ compile_case_body(&case.body, result_vt, body, ctx, state)
898
1009
  // A binding arm always matches — any following cases are unreachable.
899
1010
  }
900
1011
  }
@@ -905,17 +1016,17 @@ fn compile_match_arms(
905
1016
  Instruction::LocalGet(scratch_local).encode(body);
906
1017
  Instruction::I64Const(*n).encode(body);
907
1018
  Instruction::I64Eq.encode(body);
908
- Instruction::If(BlockType::Empty).encode(body);
1019
+ Instruction::If(block_type_for(result_vt)).encode(body);
909
- compile_block(&case.body, body, ctx, state)?;
1020
+ compile_case_body(&case.body, result_vt, body, ctx, state)?;
910
1021
  Instruction::Else.encode(body);
911
- compile_match_arms(rest, subject_vt, scratch_local, body, ctx, state)?;
1022
+ compile_match_arms(rest, subject_vt, scratch_local, result_vt, body, ctx, state)?;
912
1023
  Instruction::End.encode(body);
913
1024
  Ok(())
914
1025
  }
915
1026
  ast::CasePattern::String(_) => Err("codegen: string match patterns are not yet supported".to_string()),
916
1027
  ast::CasePattern::Float(_) => Err("codegen: float match patterns are not yet supported".to_string()),
917
1028
  ast::CasePattern::Class { name, fields } => {
918
- compile_variant_constructor_arm(name, fields, subject_vt, scratch_local, case, rest, body, ctx, state)
1029
+ compile_variant_constructor_arm(name, fields, subject_vt, scratch_local, result_vt, case, rest, body, ctx, state)
919
1030
  }
920
1031
  }
921
1032
  }
@@ -925,6 +1036,7 @@ fn compile_variant_eq_arm(
925
1036
  name: &str,
926
1037
  subject_vt: ValType,
927
1038
  scratch_local: u32,
1039
+ result_vt: Option<ValType>,
928
1040
  case: &ast::Case,
929
1041
  rest: &[ast::Case],
930
1042
  body: &mut Vec<u8>,
@@ -942,10 +1054,10 @@ fn compile_variant_eq_arm(
942
1054
  Instruction::LocalGet(scratch_local).encode(body);
943
1055
  Instruction::I32Const(tag).encode(body);
944
1056
  Instruction::I32Eq.encode(body);
945
- Instruction::If(BlockType::Empty).encode(body);
1057
+ Instruction::If(block_type_for(result_vt)).encode(body);
946
- compile_block(&case.body, body, ctx, state)?;
1058
+ compile_case_body(&case.body, result_vt, body, ctx, state)?;
947
1059
  Instruction::Else.encode(body);
948
- compile_match_arms(rest, subject_vt, scratch_local, body, ctx, state)?;
1060
+ compile_match_arms(rest, subject_vt, scratch_local, result_vt, body, ctx, state)?;
949
1061
  Instruction::End.encode(body);
950
1062
  Ok(())
951
1063
  }
@@ -956,6 +1068,7 @@ fn compile_variant_constructor_arm(
956
1068
  fields: &[ast::CasePattern],
957
1069
  subject_vt: ValType,
958
1070
  scratch_local: u32,
1071
+ result_vt: Option<ValType>,
959
1072
  case: &ast::Case,
960
1073
  rest: &[ast::Case],
961
1074
  body: &mut Vec<u8>,
@@ -994,7 +1107,7 @@ fn compile_variant_constructor_arm(
994
1107
  Instruction::Else.encode(body);
995
1108
  Instruction::I32Const(0).encode(body);
996
1109
  Instruction::End.encode(body);
997
- Instruction::If(BlockType::Empty).encode(body);
1110
+ Instruction::If(block_type_for(result_vt)).encode(body);
998
1111
  for (i, (pat, field_ty)) in fields.iter().zip(field_types.iter()).enumerate() {
999
1112
  let bind_name = match pat {
1000
1113
  ast::CasePattern::Name(n) => Some(n.as_str()),
@@ -1018,9 +1131,9 @@ fn compile_variant_constructor_arm(
1018
1131
  ctx.type_env.borrow_mut().insert(n.to_string(), TypeScheme::mono(field_ty.clone()));
1019
1132
  }
1020
1133
  }
1021
- compile_block(&case.body, body, ctx, state)?;
1134
+ compile_case_body(&case.body, result_vt, body, ctx, state)?;
1022
1135
  Instruction::Else.encode(body);
1023
- compile_match_arms(rest, subject_vt, scratch_local, body, ctx, state)?;
1136
+ compile_match_arms(rest, subject_vt, scratch_local, result_vt, body, ctx, state)?;
1024
1137
  Instruction::End.encode(body);
1025
1138
  Ok(())
1026
1139
  }
plum-wasm-codegen/tests/codegen_tests.rs CHANGED
@@ -552,3 +552,135 @@ main() -> Int =
552
552
  let bytes = compile_source(&source).expect("compile failed");
553
553
  assert_eq!(run_main(&bytes), 42);
554
554
  }
555
+
556
+ #[test]
557
+ fn tail_match_without_return_runs_correctly() {
558
+ let src = "\
559
+ bindExample(n: Int) -> Int =
560
+ match n
561
+ x =>
562
+ x
563
+
564
+ main() -> Int =
565
+ bindExample(5)
566
+ ";
567
+ let source = parse(src);
568
+ let bytes = compile_source(&source).expect("compile failed");
569
+ assert_eq!(run_main(&bytes), 5);
570
+ }
571
+
572
+ #[test]
573
+ fn tail_if_without_return_runs_correctly() {
574
+ let src = "\
575
+ abs(n: Int) -> Int =
576
+ if n < 0
577
+ -n
578
+ else
579
+ n
580
+
581
+ main() -> Int =
582
+ abs(-7)
583
+ ";
584
+ let source = parse(src);
585
+ let bytes = compile_source(&source).expect("compile failed");
586
+ assert_eq!(run_main(&bytes), 7);
587
+ }
588
+
589
+ #[test]
590
+ fn tail_if_nested_inside_match_arm_without_return_runs_correctly() {
591
+ let src = "\
592
+ classify(n: Int) -> Int =
593
+ match n
594
+ 0 =>
595
+ 1
596
+ x =>
597
+ if x < 0
598
+ -1
599
+ else
600
+ 2
601
+
602
+ main() -> Int =
603
+ classify(-5)
604
+ ";
605
+ let source = parse(src);
606
+ let bytes = compile_source(&source).expect("compile failed");
607
+ assert_eq!(run_main(&bytes), -1);
608
+ }
609
+
610
+ #[test]
611
+ fn tail_match_mixing_return_and_bare_expr_arms_runs_correctly() {
612
+ let src = "\
613
+ describe(n: Int) -> Int =
614
+ match n
615
+ 0 =>
616
+ return 100
617
+ x =>
618
+ x * 2
619
+
620
+ main() -> Int =
621
+ describe(21)
622
+ ";
623
+ let source = parse(src);
624
+ let bytes = compile_source(&source).expect("compile failed");
625
+ assert_eq!(run_main(&bytes), 42);
626
+ }
627
+
628
+ #[test]
629
+ fn tail_enum_match_without_return_runs_correctly() {
630
+ let src = "\
631
+ enum Option =
632
+ | Some(Int)
633
+ | None
634
+
635
+ unwrapOr(o: Option, default: Int) -> Int =
636
+ match o
637
+ Some(v) =>
638
+ v
639
+ None =>
640
+ default
641
+
642
+ main() -> Int =
643
+ unwrapOr(Some(9), 0)
644
+ ";
645
+ let source = parse(src);
646
+ let bytes = compile_source(&source).expect("compile failed");
647
+ assert_eq!(run_main(&bytes), 9);
648
+ }
649
+
650
+ #[test]
651
+ fn tail_if_without_else_is_a_clear_error() {
652
+ let src = "\
653
+ bad(n: Int) -> Int =
654
+ if n < 0
655
+ return 1
656
+ ";
657
+ let source = parse(src);
658
+ let err = compile_source(&source).expect_err("if without else in value position must be a clear error, not invalid wasm");
659
+ assert!(err.contains("doesn't produce a return value"), "got: {}", err);
660
+ }
661
+
662
+ #[test]
663
+ fn tail_match_non_exhaustive_is_a_clear_error() {
664
+ let src = "\
665
+ bad(n: Int) -> Int =
666
+ match n
667
+ 0 =>
668
+ 1
669
+ ";
670
+ let source = parse(src);
671
+ let err = compile_source(&source).expect_err("non-exhaustive match in value position must be a clear error, not invalid wasm");
672
+ assert!(err.contains("doesn't produce a return value"), "got: {}", err);
673
+ }
674
+
675
+ #[test]
676
+ fn tail_match_arm_ending_in_non_value_statement_is_a_clear_error() {
677
+ let src = "\
678
+ bad(n: Int) -> Int =
679
+ match n
680
+ x =>
681
+ y = x
682
+ ";
683
+ let source = parse(src);
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
+ assert!(err.contains("doesn't produce a return value"), "got: {}", err);
686
+ }