From 9b496979f4735b09ae71651f89f6d4f1829cde5c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 13:57:28 +0000 Subject: [PATCH 1/2] docs(ci): record the Actions allow-list of this repository A reviewed copy of the repository setting (Settings > Actions > General). It has only what the workflows here and the pinned standards workflows use, plus two entries kept by owner decision. GitHub does not read the file; the owner changes the setting by hand. Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_0136P7ZmKrYLDC4WHi2yt11u --- .github/actions-allowlist.txt | 41 +++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/actions-allowlist.txt diff --git a/.github/actions-allowlist.txt b/.github/actions-allowlist.txt new file mode 100644 index 0000000..70b6902 --- /dev/null +++ b/.github/actions-allowlist.txt @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: MPL-2.0 +# Owner: Jonathan D.A. Jewell +# +# Reviewed copy of the Actions allow-list of this repository: +# Settings > Actions > General > "Allow ... specified actions and reusable +# workflows". GitHub does not read this file; the owner changes the setting +# by hand. When a workflow needs a new action, add the entry here in the +# same pull request, then update the setting. +# +# Also required on that page: "Allow actions created by GitHub" +# (actions/*, github/codeql-action). +# +# Source of the entries: each `uses:` in .github/workflows/*.yml, and the +# `uses` list of metadatastician/standards in .github/workflows/actions.lock +# (what the reusable workflows use). To paste the list into the settings +# page, take the lines that do not start with `#`. + +# This repository's own workflows. +Swatinem/rust-cache@*, +swatinem/rust-cache@*, +dawidd6/action-send-mail@*, +dtolnay/rust-toolchain@*, +mlugg/setup-zig@*, +peter-evans/repository-dispatch@*, + +# The reusable workflows of the standards repository, and what they use. +metadatastician/standards/.github/workflows/*@*, +denoland/setup-deno@*, +editorconfig-checker/action-editorconfig-checker@*, +erlef/setup-beam@*, +goto-bus-stop/setup-zig@*, +ossf/scorecard-action@*, +oven-sh/setup-bun@*, +webfactory/ssh-agent@*, + +# The owner's own repositories. +hyperpolymath/*, + +# No workflow uses these yet. Kept by owner decision (2026-11-10). +extractions/setup-just@*, +ludeeus/action-shellcheck@* From 35aa73bc3131b138ffd3ad11b70423d8a0a7cdd6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 13:57:28 +0000 Subject: [PATCH 2/2] fix(wasm,driver): scoped arm binders, typed tuple patterns, review findings - The binder of a `case` arm and the names a `match` arm binds are now in scope in that arm only (`let` already did this). A binder stayed bound after the `case`, so `let x = 5`, then a match with `Yes(x)`, then `r + x` read the binder: 14, not 12. With a binder of another wasm type the module was invalid. Older than this pull request. - A tuple pattern reads the 8-byte slots of the tuple, each at its own type. It followed pair pointers, which a tuple does not have. A tuple pattern inside a constructor pattern has no types to use and is refused. - `compile_module_with_main` returns an unsupported-construct error from the typed validation of the module; it took the fallback. - `qualify_functions` is three functions (SonarCloud cognitive complexity). No change in behaviour. - TEST-NEEDS.adoc has one test count (600) in its three places. Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_0136P7ZmKrYLDC4WHi2yt11u --- TEST-NEEDS.adoc | 6 +- src/ephapax-cli/tests/run_wasm.rs | 30 +++++ src/ephapax-cli/tests/wasm_e2e.rs | 109 ++++++++++++++++++ src/ephapax-driver/src/qualify.rs | 62 +++++----- src/ephapax-wasm/src/lib.rs | 181 ++++++++++++++++++++++++------ 5 files changed, 325 insertions(+), 63 deletions(-) diff --git a/TEST-NEEDS.adoc b/TEST-NEEDS.adoc index 168c5c3..2677784 100644 --- a/TEST-NEEDS.adoc +++ b/TEST-NEEDS.adoc @@ -21,9 +21,9 @@ compliance, 16 tests) performance, correctness, 13 tests) * Benchmarks: `+src/ephapax-parser/benches/parse_bench.rs+`, `+src/ephapax-vram-cache/benches/cache_bench.rs+` -* Total: *590 tests* (`+cargo test --workspace --all-targets+`); +* Total: *600 tests* (`+cargo test --workspace --all-targets+`); pass/fail enforced by `+rust-ci.yml+` -* Documented all-target tests: 596 +* Documented all-target tests: 600 === CRG Testing Taxonomy — Status @@ -83,7 +83,7 @@ eliminator-fork invariant + its gated capstone, per PROOF-NEEDS.adoc §4) ==== Build & Execution * [x] cargo build — compiles -* [x] cargo test –workspace –all-targets — 467 pass, 0 fail +* [x] cargo test –workspace –all-targets — 600 pass, 0 fail * [x] idris2/ephapax-parse-tests.ipkg — compiles AND runs green (2026-07-07, idris2 0.8.0 + zig-built libephapax_tokbuf.so); CI-gated compile-only by abi-verify.yml `+idris2-parse-front+` (runtime run stays diff --git a/src/ephapax-cli/tests/run_wasm.rs b/src/ephapax-cli/tests/run_wasm.rs index 371e1d9..cf4f846 100644 --- a/src/ephapax-cli/tests/run_wasm.rs +++ b/src/ephapax-cli/tests/run_wasm.rs @@ -248,3 +248,33 @@ fn pair_patterns_bind_each_component_at_its_type() { } } } + +/// The binder of a `case` arm is in scope in that arm only. It used to +/// stay bound after the `case`, so a later use of an outer variable of +/// the same name read the binder. +#[test] +fn an_arm_binder_does_not_replace_an_outer_variable() { + let tmp = tempfile::tempdir().expect("tempdir"); + for (payload, value, arm, want) in [ + ("I32", "7", "x", "12"), + // The binder has another wasm type than the outer variable. + ("I64", "7000000000", "1", "6"), + ] { + std::fs::write( + tmp.path().join("scope.eph"), + format!( + "module scope\n\ndata Opt = Yes({payload}) | No\n\nfn main(): I32 =\n \ + let x = 5\n let r = match Yes({value}) of\n | Yes(x) => {arm}\n \ + | No => 0\n end\n r + x\n" + ), + ) + .expect("write program"); + for args in [&["run", "scope.eph"][..], &["run", "--interp", "scope.eph"]] { + assert_eq!( + stdout_of(tmp.path(), args), + want, + "{payload} under {args:?}" + ); + } + } +} diff --git a/src/ephapax-cli/tests/wasm_e2e.rs b/src/ephapax-cli/tests/wasm_e2e.rs index 5c34fa2..ad31a39 100644 --- a/src/ephapax-cli/tests/wasm_e2e.rs +++ b/src/ephapax-cli/tests/wasm_e2e.rs @@ -131,3 +131,112 @@ fn e2e_wasm_header_valid() { assert_eq!(&wasm[0..4], b"\x00asm", "bad WASM magic"); assert_eq!(&wasm[4..8], &[1, 0, 0, 0], "bad WASM version"); } + +// ========================================================================= +// Core `match` on a hand-built module: the result of `main` is checked +// ========================================================================= + +/// Compile a module whose only function is `main(): = body`, run +/// it, and return the result of `main`. +fn run_main(ret_ty: ephapax_syntax::Ty, body: ephapax_syntax::Expr) -> R { + use ephapax_syntax::{Decl, Module, Visibility}; + let module = Module { + name: "t".into(), + imports: vec![], + decls: vec![Decl::Fn { + name: "main".into(), + visibility: Visibility::Private, + type_params: vec![], + params: vec![], + ret_ty, + body, + }], + }; + let wasm = ephapax_wasm::compile_module(&module).expect("the module compiles"); + wasmparser::validate(&wasm).expect("emitted wasm failed wasmparser validation"); + + let engine = wasmtime::Engine::default(); + let module = wasmtime::Module::new(&engine, &wasm).expect("WASM module creation failed"); + let mut store = wasmtime::Store::new(&engine, ()); + let mut linker = wasmtime::Linker::new(&engine); + linker + .func_wrap("env", "print_i32", |_val: i32| {}) + .expect("failed to define print_i32"); + linker + .func_wrap("env", "print_string", |_ptr: i32, _len: i32| {}) + .expect("failed to define print_string"); + let instance = linker + .instantiate(&mut store, &module) + .expect("WASM instantiation failed"); + instance + .get_typed_func::<(), R>(&mut store, "main") + .expect("main function not found") + .call(&mut store, ()) + .expect("WASM execution trapped") +} + +/// A tuple pattern reads the 8-byte slots of the tuple, each at its own +/// type. It used to follow pair pointers, which a tuple does not have. +#[test] +fn e2e_tuple_pattern_reads_the_tuple_slots() { + use ephapax_syntax::{BaseTy, Expr, ExprKind, Literal, MatchArm, Pattern, Ty}; + let lit = |l| Expr::dummy(ExprKind::Lit(l)); + // match (1, 5000000000, 3) of | (a, b, c) => b end + let body = Expr::dummy(ExprKind::Match { + scrutinee: Box::new(Expr::dummy(ExprKind::TupleLit(vec![ + lit(Literal::I32(1)), + lit(Literal::I64(5_000_000_000)), + lit(Literal::I32(3)), + ]))), + arms: vec![MatchArm { + pattern: Pattern::Tuple(vec![ + Pattern::Var("a".into()), + Pattern::Var("b".into()), + Pattern::Var("c".into()), + ]), + guard: None, + body: Expr::dummy(ExprKind::Var("b".into())), + }], + }); + let result: i64 = run_main(Ty::Base(BaseTy::I64), body); + assert_eq!(result, 5_000_000_000); +} + +/// A name that one `match` arm binds is not in scope in the next arm. +/// The next arm used to read the binder of the arm that did not match. +#[test] +fn e2e_match_arm_binder_ends_with_its_arm() { + use ephapax_syntax::{BaseTy, Expr, ExprKind, Literal, MatchArm, Pattern, Ty}; + let lit = |n| Expr::dummy(ExprKind::Lit(Literal::I32(n))); + let var = |name: &str| Expr::dummy(ExprKind::Var(name.into())); + // let y = 5 in match (1, 2) of | (0, y) => y | _ => y end + let matched = Expr::dummy(ExprKind::Match { + scrutinee: Box::new(Expr::dummy(ExprKind::Pair { + left: Box::new(lit(1)), + right: Box::new(lit(2)), + })), + arms: vec![ + MatchArm { + pattern: Pattern::Pair( + Box::new(Pattern::Literal(Literal::I32(0))), + Box::new(Pattern::Var("y".into())), + ), + guard: None, + body: var("y"), + }, + MatchArm { + pattern: Pattern::Wildcard, + guard: None, + body: var("y"), + }, + ], + }); + let body = Expr::dummy(ExprKind::Let { + name: "y".into(), + ty: None, + value: Box::new(lit(5)), + body: Box::new(matched), + }); + let result: i32 = run_main(Ty::Base(BaseTy::I32), body); + assert_eq!(result, 5); +} diff --git a/src/ephapax-driver/src/qualify.rs b/src/ephapax-driver/src/qualify.rs index d87d37a..3bf2b41 100644 --- a/src/ephapax-driver/src/qualify.rs +++ b/src/ephapax-driver/src/qualify.rs @@ -51,13 +51,26 @@ fn module_key(name: &str) -> String { /// its names (they are the entry points of the program); after it, the /// first module that declares a name keeps it. pub(crate) fn qualify_functions(modules: &mut [LoadedModule]) { - let root = modules.len().saturating_sub(1); + let qualified = qualified_names(modules); + if qualified.iter().all(Renames::is_empty) { + return; + } + let scopes = scopes(modules, &qualified); + for (m, scope) in modules.iter_mut().zip(&scopes) { + if !scope.is_empty() { + m.core.decls.iter_mut().for_each(|d| rename_decl(d, scope)); + } + } +} - // The new name of each function that must change, by module. +/// For each module, the new name of each of its functions that must +/// change: `::`. +fn qualified_names(modules: &[LoadedModule]) -> Vec { + let root = modules.len().saturating_sub(1); let mut owner = HashMap::new(); let mut qualified = vec![Renames::new(); modules.len()]; for i in std::iter::once(root).chain(0..root) { - let Some(m) = modules.get(i) else { return }; + let Some(m) = modules.get(i) else { break }; for (name, _, function) in callables(&m.core) { if function && *owner.entry(name).or_insert(i) != i { let new = format!("{}::{name}", m.logical_path); @@ -65,19 +78,19 @@ pub(crate) fn qualify_functions(modules: &mut [LoadedModule]) { } } } - if qualified.iter().all(Renames::is_empty) { - return; - } + qualified +} - // What each name means in each module, in the order the type checker - // brings names into scope: the imports in order (a later one shadows - // an earlier one), then the declarations of the module. +/// For each module, what each renamed name means there, in the order the +/// type checker brings names into scope: the imports in order (a later +/// one shadows an earlier one), then the declarations of the module. +fn scopes(modules: &[LoadedModule], qualified: &[Renames]) -> Vec { let index: HashMap = modules .iter() .enumerate() .map(|(i, m)| (module_key(&m.core.name), i)) .collect(); - let scopes: Vec = modules + modules .iter() .enumerate() .map(|(i, m)| { @@ -103,25 +116,20 @@ pub(crate) fn qualify_functions(modules: &mut [LoadedModule]) { } scope }) - .collect(); + .collect() +} - for (m, scope) in modules.iter_mut().zip(&scopes) { - if scope.is_empty() { - continue; - } - for decl in &mut m.core.decls { - match decl { - Decl::Fn { - name, params, body, .. - } => { - rename(name, scope); - params.iter_mut().for_each(|(p, _)| rename(p, scope)); - rename_expr(body, scope); - } - Decl::Const { value, .. } => rename_expr(value, scope), - Decl::Type { .. } | Decl::Extern { .. } | Decl::Data { .. } => {} - } +fn rename_decl(decl: &mut Decl, scope: &Renames) { + match decl { + Decl::Fn { + name, params, body, .. + } => { + rename(name, scope); + params.iter_mut().for_each(|(p, _)| rename(p, scope)); + rename_expr(body, scope); } + Decl::Const { value, .. } => rename_expr(value, scope), + Decl::Type { .. } | Decl::Extern { .. } | Decl::Data { .. } => {} } } diff --git a/src/ephapax-wasm/src/lib.rs b/src/ephapax-wasm/src/lib.rs index 352ca93..075c60b 100644 --- a/src/ephapax-wasm/src/lib.rs +++ b/src/ephapax-wasm/src/lib.rs @@ -249,16 +249,14 @@ fn first_unsupported_in(expr: &Expr, types: Option<&ExprTypes>) -> Option types .and_then(|t| t.get(&scrutinee.id)) - .filter(|ty| { - arms.iter() - .any(|arm| pattern_reads_wide_as_i32(&arm.pattern, ty)) - }) + .filter(|ty| arms.iter().any(|arm| pattern_not_lowered(&arm.pattern, ty))) .map(|ty| { unsupported( &format!( - "`match` with a tuple, constructor or literal pattern on a value \ - with an I64, F32 or F64 component (type {ty:?}); use a pair \ - pattern, `fst`, `snd`, `.N` or `case`" + "`match` with a pattern that has no lowering at its type: a \ + constructor or literal pattern on a value with an I64, F32 or \ + F64 component, or a tuple pattern inside a constructor pattern \ + (type {ty:?}); use a pair pattern, `fst`, `snd`, `.N` or `case`" ), expr.span, ) @@ -456,6 +454,27 @@ impl LocalTracker { idx } + /// The binding of `name`, for [`Self::restore`] when a scope that + /// shadows it ends. + fn binding(&self, name: &str) -> (Option, Option) { + ( + self.name_to_idx.get(name).copied(), + self.name_to_type.get(name).copied(), + ) + } + + /// End a scope of `name`: put back the binding that it shadowed. + fn restore(&mut self, name: &str, (idx, ty): (Option, Option)) { + match idx { + Some(idx) => self.name_to_idx.insert(name.to_string(), idx), + None => self.name_to_idx.remove(name), + }; + match ty { + Some(ty) => self.name_to_type.insert(name.to_string(), ty), + None => self.name_to_type.remove(name), + }; + } + /// Allocate an anonymous i32 temporary. fn temp(&mut self) -> u32 { self.temp_typed(ValType::I32) @@ -976,8 +995,12 @@ impl Codegen { return Err(err); } // Try full module compilation first; fall back to expression mode. - if let Ok(bytes) = self.compile_ast_module(module) { - return Ok(bytes); + // An unsupported construct that only the types show (a wide + // lambda) is a hard error too. + match self.compile_ast_module(module) { + Ok(bytes) => return Ok(bytes), + Err(e) if e.0.starts_with(UNSUPPORTED_PREFIX) => return Err(e), + Err(_) => {} } // Fallback: the main body as a standalone expression, checked @@ -2427,22 +2450,14 @@ impl Codegen { self.compile_expr(func, value); // Bind it to a local - let outer_idx = self.locals.name_to_idx.get(name).copied(); - let outer_ty = self.locals.name_to_type.get(name).copied(); + let outer = self.locals.binding(name); let local_idx = self.locals.bind_typed(name, is_linear, local_ty); func.instruction(&Instruction::LocalSet(local_idx)); // Compile the body self.compile_expr(func, body); - self.locals.name_to_idx.remove(name); - self.locals.name_to_type.remove(name); - if let Some(idx) = outer_idx { - self.locals.name_to_idx.insert(name.to_string(), idx); - } - if let Some(ty) = outer_ty { - self.locals.name_to_type.insert(name.to_string(), ty); - } + self.locals.restore(name, outer); // Handle unconsumed linear locals (mode-aware): // - Linear mode: Type checker already validated consumption, no action needed @@ -2929,18 +2944,23 @@ impl Codegen { // Right branch (tag == 1) func.instruction(&Instruction::LocalGet(scrutinee_local)); emit_typed_load(func, right_ty, sum_payload_offset(right_ty)); + // Each binder is in scope in its own arm only. + let outer = self.locals.binding(right_var); let right_local = self.locals.bind_typed(right_var, false, right_ty); func.instruction(&Instruction::LocalSet(right_local)); self.compile_expr(func, right_body); + self.locals.restore(right_var, outer); func.instruction(&Instruction::Else); // Left branch (tag == 0) func.instruction(&Instruction::LocalGet(scrutinee_local)); emit_typed_load(func, left_ty, sum_payload_offset(left_ty)); + let outer = self.locals.binding(left_var); let left_local = self.locals.bind_typed(left_var, false, left_ty); func.instruction(&Instruction::LocalSet(left_local)); self.compile_expr(func, left_body); + self.locals.restore(left_var, outer); func.instruction(&Instruction::End); } @@ -2984,10 +3004,11 @@ impl Codegen { /// through on inner refutation (needed once #66 admits patterns /// like `Some(Some(_))` + `Some(None)` + `None`). fn compile_match(&mut self, func: &mut Function, scrutinee: &Expr, arms: &[MatchArm]) { - // A variable, a wildcard and a pair pattern are lowered at the type - // of the value. A tuple, constructor or literal pattern reads i32 - // values only; on a value with an I64, F32 or F64 component it - // never gets here: `first_unsupported_typed` refuses it. + // A variable, a wildcard, a pair pattern and a tuple pattern are + // lowered at the type of the value. A constructor or literal + // pattern reads i32 values only; on a value with an I64, F32 or F64 + // component it never gets here: `first_unsupported_typed` refuses + // it (`pattern_not_lowered`). use wasm_encoder::BlockType; let scrut_ty = self.ty_of(scrutinee).cloned(); let scrut_valtype = self.valtype_of(scrutinee); @@ -3034,6 +3055,11 @@ impl Codegen { func.instruction(&Instruction::Block(BlockType::Result(result))); for arm in arms.iter() { + // The names that the pattern binds are in scope in this arm + // only. + let bound = arm.pattern.bound_vars(); + let outer: Vec<_> = bound.iter().map(|v| self.locals.binding(v)).collect(); + // Open $arm_i_fail block. Inside, depth 0 = $arm_i_fail, // depth 1 = $done. func.instruction(&Instruction::Block(BlockType::Empty)); @@ -3075,6 +3101,9 @@ impl Codegen { func.instruction(&Instruction::Br(1)); func.instruction(&Instruction::End); // close $arm_i_fail + for (name, outer) in bound.iter().zip(outer) { + self.locals.restore(name, outer); + } } // No arm matched at runtime — the typechecker rejects non- @@ -3184,9 +3213,23 @@ impl Codegen { self.compile_sub_pattern(func, left, left_local, left_ty, fail_depth); self.compile_sub_pattern(func, right, right_local, right_ty, fail_depth); } - Pattern::Tuple(parts) => { - self.destructure_payload(func, parts, value_local, fail_depth); - } + Pattern::Tuple(parts) => match ty.map(peel_borrows) { + // A tuple is a pointer to one 8-byte slot for each + // element, stored at its own type (`compile_tuple_lit`). + Some(Ty::Tuple(elems)) if elems.len() == parts.len() => { + for (i, (part, elem)) in parts.iter().zip(elems).enumerate() { + let valtype = ty_to_valtype(elem); + let local = self.locals.temp_typed(valtype); + func.instruction(&Instruction::LocalGet(value_local)); + emit_typed_load(func, valtype, i as u64 * TUPLE_SLOT as u64); + func.instruction(&Instruction::LocalSet(local)); + self.compile_sub_pattern(func, part, local, Some(elem), fail_depth); + } + } + // No type (below a constructor pattern): `pattern_not_lowered` + // refuses a tuple pattern there. + _ => self.destructure_payload(func, parts, value_local, fail_depth), + }, Pattern::Constructor { ctor, args } => { let Some(ci) = self.data_ctors.get(ctor.as_str()).cloned() else { // Unknown ctor — typechecker should have rejected. @@ -3919,17 +3962,34 @@ fn peel_borrows(ty: &Ty) -> &Ty { } } -/// True when the lowering of `pattern` against a value of type `ty` -/// would read an I64, F32 or F64 component as an i32. A variable, a -/// wildcard and a pair pattern are lowered at the type of the value; a -/// tuple, constructor or literal pattern reads i32 values only. -fn pattern_reads_wide_as_i32(pattern: &Pattern, ty: &Ty) -> bool { +/// True when codegen has no correct lowering of `pattern` against a value +/// of type `ty`. A variable, a wildcard, a pair pattern and a tuple +/// pattern are lowered at the type of the value. A constructor or literal +/// pattern reads i32 values. Below a constructor pattern the lowering has +/// no types, so a tuple pattern there has no lowering. +fn pattern_not_lowered(pattern: &Pattern, ty: &Ty) -> bool { match (pattern, peel_borrows(ty)) { (Pattern::Wildcard | Pattern::Var(_) | Pattern::Unit, _) => false, (Pattern::Pair(l, r), Ty::Prod { left, right }) => { - pattern_reads_wide_as_i32(l, left) || pattern_reads_wide_as_i32(r, right) + pattern_not_lowered(l, left) || pattern_not_lowered(r, right) } - _ => has_non_i32_component(ty), + (Pattern::Tuple(parts), Ty::Tuple(elems)) if parts.len() == elems.len() => parts + .iter() + .zip(elems) + .any(|(part, elem)| pattern_not_lowered(part, elem)), + (Pattern::Constructor { args, .. }, ty) => { + has_non_i32_component(ty) || args.iter().any(has_tuple_pattern) + } + (_, ty) => has_non_i32_component(ty), + } +} + +fn has_tuple_pattern(pattern: &Pattern) -> bool { + match pattern { + Pattern::Tuple(_) => true, + Pattern::Pair(l, r) => has_tuple_pattern(l) || has_tuple_pattern(r), + Pattern::Constructor { args, .. } => args.iter().any(has_tuple_pattern), + Pattern::Wildcard | Pattern::Var(_) | Pattern::Unit | Pattern::Literal(_) => false, } } @@ -4812,6 +4872,61 @@ mod tests { .compile_module_with_main(&module, &main_body) .expect("the main body compiles on its own"); validate_wasm(&wasm); + + // A module that is well typed but has a construct with no lowering + // is an error, not a reason to take the fallback. + // fn f(): I32 = match 5000000000 of | 5000000000 => 1 | _ => 0 end + let arm = |pattern, n| MatchArm { + pattern, + guard: None, + body: e(ExprKind::Lit(Literal::I32(n))), + }; + let unsupported = AstModule { + name: "test".into(), + imports: vec![], + decls: vec![Decl::Fn { + name: "f".into(), + visibility: Visibility::Private, + type_params: vec![], + params: vec![], + ret_ty: Ty::Base(BaseTy::I32), + body: e(ExprKind::Match { + scrutinee: Box::new(e(ExprKind::Lit(Literal::I64(5_000_000_000)))), + arms: vec![ + arm(P::Literal(Literal::I64(5_000_000_000)), 1), + arm(P::Wildcard, 0), + ], + }), + }], + }; + let err = Codegen::new() + .compile_module_with_main(&unsupported, &main_body) + .expect_err("no fallback"); + assert!(err.0.starts_with(UNSUPPORTED_PREFIX), "{}", err.0); + } + + /// The patterns that codegen lowers at the type of the value, and the + /// ones that it refuses. + #[test] + fn pattern_lowering_coverage() { + let i32_ty = || Ty::Base(BaseTy::I32); + let i64_ty = || Ty::Base(BaseTy::I64); + let vars = |n: usize| vec![P::Wildcard; n]; + let tuple_ty = Ty::Tuple(vec![i32_ty(), i64_ty(), i32_ty()]); + // A tuple pattern is typed, also with a wide element. + assert!(!pattern_not_lowered(&P::Tuple(vars(3)), &tuple_ty)); + // Below a constructor pattern there are no types. + let ctor = |args| P::Constructor { + ctor: "C".into(), + args, + }; + assert!(pattern_not_lowered( + &ctor(vec![P::Tuple(vars(3))]), + &i32_ty() + )); + assert!(!pattern_not_lowered(&ctor(vars(1)), &i32_ty())); + // A literal pattern reads an i32. + assert!(pattern_not_lowered(&P::Literal(Literal::I64(1)), &i64_ty())); } #[test]