diff --git a/.editorconfig b/.editorconfig index 9a26f765..32d2f240 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,5 +1,6 @@ # SPDX-License-Identifier: MPL-2.0 # .editorconfig — RSR standard editor configuration +# https://editorconfig.org root = true @@ -11,3 +12,44 @@ end_of_line = lf trim_trailing_whitespace = true insert_final_newline = true +# Prose formats rely on trailing double-space for hard line breaks. +[*.md] +trim_trailing_whitespace = false + +[*.adoc] +trim_trailing_whitespace = false + +# Per-language indent widths. +[*.rs] +indent_size = 4 + +[*.zig] +indent_size = 4 + +[*.{ada,adb,ads}] +indent_size = 3 + +[*.{ex,exs}] +indent_size = 2 + +[*.hs] +indent_size = 2 + +[*.{res,resi}] +indent_size = 2 + +[*.ncl] +indent_size = 2 + +[*.rkt] +indent_size = 2 + +[*.scm] +indent_size = 2 + +[*.nix] +indent_size = 2 + +[{Justfile,justfile}] +indent_style = space +indent_size = 4 diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 5eaf379c..36c7b5e1 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -2,6 +2,17 @@ # Rust CI — thin wrapper calling the shared estate reusable in # hyperpolymath/standards. Configure once, propagate everywhere. # See: docs/CI-REUSABLE-WORKFLOWS.adoc in standards. +# +# DO NOT reintroduce a job-level `if: hashFiles('Cargo.toml') != ''` guard here. +# Job-level `if:` is evaluated server-side before any checkout, so hashFiles() +# has no workspace to hash. This workflow carried that guard on both local jobs +# and, as a result, failed 60/60 runs at 0s with zero jobs and no logs — a +# startup failure, not a test failure. GitHub reported the workflow's name as +# `.github/workflows/rust-ci.yml` (the path-fallback used when a file has never +# been parsed) rather than the declared `Rust CI`, which is how it was found. +# Net effect: this repo's Rust build/test gate never ran between 2026-06-27 and +# 2026-07-27. If a Cargo.toml presence check is ever needed, follow the estate +# reusable and gate on a `detect` job output, not on hashFiles() at job level. name: Rust CI on: @@ -24,7 +35,6 @@ jobs: name: Cargo build + test (ephapax-cli, --no-default-features) runs-on: ubuntu-latest timeout-minutes: 20 - if: hashFiles('Cargo.toml') != '' # Proves ephapax can be built and tested with zero git dep on the # sibling `hyperpolymath/typed-wasm` repo. Enforces the estate @@ -64,7 +74,6 @@ jobs: name: wasm-tools validate (emitted modules) runs-on: ubuntu-latest timeout-minutes: 20 - if: hashFiles('Cargo.toml') != '' # Structurally validates every module ephapax emits for the fixture # corpus (`just validate-wasm`), catching codegen that produces an diff --git a/.gitignore b/.gitignore index 2e96c3a4..e918aa0c 100644 --- a/.gitignore +++ b/.gitignore @@ -97,3 +97,36 @@ dist/ *.cmi *.cmo *.cmx + +# Additional build/tooling artefacts (salvaged from the 2026-07-26 sweep; +# `.claude/` and `.editorconfig` were dropped from that set — both are TRACKED +# in this repo and ignoring them would hide real changes from `git status`). +*.ali +*.chpl.tmp.* +*.db +*.db-journal +*.db-shm +*.db-wal +*.ez +*.jl.cov +*.jl.mem +*.py[cod] +.venv/ +/.elixir_ls/ +/.stack-work/ +/Manifest.toml +/_build/ +/bin/ +/build/ +/cover/ +/dist-newstyle/ +/dist/ +/doc/ +/exports/*.json +/exports/*.lgt +/obj/ +/out/ +__pycache__/ +composer/*.beam +composer/build/ +erl_crash.dump diff --git a/ephapax-linear/src/tests.rs b/ephapax-linear/src/tests.rs index efafa6dd..79f35f3e 100644 --- a/ephapax-linear/src/tests.rs +++ b/ephapax-linear/src/tests.rs @@ -422,19 +422,29 @@ fn p2p_contraction_always_rejected_both_disciplines() { let mut linear = LinearChecker::new(); let result = linear.check(&expr); - assert!(result.is_err(), "P2P linear[{i}]: double use must be rejected"); + assert!( + result.is_err(), + "P2P linear[{i}]: double use must be rejected" + ); let violations = result.unwrap_err(); assert!( - violations.iter().any(|v| matches!(v, DisciplineViolation::Contraction { .. })), + violations + .iter() + .any(|v| matches!(v, DisciplineViolation::Contraction { .. })), "P2P linear[{i}]: must report Contraction violation" ); let mut affine = AffineChecker::new(); let result = affine.check(&expr); - assert!(result.is_err(), "P2P affine[{i}]: double use must be rejected"); + assert!( + result.is_err(), + "P2P affine[{i}]: double use must be rejected" + ); let violations = result.unwrap_err(); assert!( - violations.iter().any(|v| matches!(v, DisciplineViolation::Contraction { .. })), + violations + .iter() + .any(|v| matches!(v, DisciplineViolation::Contraction { .. })), "P2P affine[{i}]: must report Contraction violation" ); } @@ -459,10 +469,15 @@ fn p2p_weakening_linear_forbidden_affine_allowed() { // Linear must reject let mut linear = LinearChecker::new(); let result = linear.check(&expr); - assert!(result.is_err(), "P2P linear[{i}]: weakening must be rejected"); + assert!( + result.is_err(), + "P2P linear[{i}]: weakening must be rejected" + ); let violations = result.unwrap_err(); assert!( - violations.iter().any(|v| matches!(v, DisciplineViolation::WeakeningForbidden { .. })), + violations + .iter() + .any(|v| matches!(v, DisciplineViolation::WeakeningForbidden { .. })), "P2P linear[{i}]: must report WeakeningForbidden" ); @@ -790,7 +805,10 @@ fn aspect_checker_stateless_between_instances() { }); let mut checker1 = LinearChecker::new(); - assert!(checker1.check(&bad_expr).is_err(), "Aspect stateless: bad expr must fail"); + assert!( + checker1.check(&bad_expr).is_err(), + "Aspect stateless: bad expr must fail" + ); // Second call with a DIFFERENT checker instance on a good expression let good_expr = e(ExprKind::LetLin { @@ -835,10 +853,7 @@ fn aspect_violations_non_empty_on_failure() { body: Box::new(unit()), }), ), - ( - "drop", - e(ExprKind::Drop(Box::new(i32_lit(3)))), - ), + ("drop", e(ExprKind::Drop(Box::new(i32_lit(3))))), ]; for (label, expr) in &bad_cases { @@ -1037,14 +1052,30 @@ fn aspect_violation_error_warning_mutually_exclusive() { use crate::DisciplineViolation; let all_violations: Vec = vec![ - DisciplineViolation::Contraction { name: "x".to_string() }, - DisciplineViolation::NotInScope { name: "y".to_string() }, - DisciplineViolation::WeakeningForbidden { name: "z".to_string() }, + DisciplineViolation::Contraction { + name: "x".to_string(), + }, + DisciplineViolation::NotInScope { + name: "y".to_string(), + }, + DisciplineViolation::WeakeningForbidden { + name: "z".to_string(), + }, DisciplineViolation::DropForbidden, - DisciplineViolation::BranchDisagreement { name: "b".to_string() }, - DisciplineViolation::RegionLeakLinear { region: "r".to_string(), name: "s".to_string() }, - DisciplineViolation::LetForLinearType { name: "t".to_string() }, - DisciplineViolation::ImplicitDropWarning { region: "r".to_string(), name: "u".to_string() }, + DisciplineViolation::BranchDisagreement { + name: "b".to_string(), + }, + DisciplineViolation::RegionLeakLinear { + region: "r".to_string(), + name: "s".to_string(), + }, + DisciplineViolation::LetForLinearType { + name: "t".to_string(), + }, + DisciplineViolation::ImplicitDropWarning { + region: "r".to_string(), + name: "u".to_string(), + }, ]; for violation in &all_violations { diff --git a/src/ephapax-cli/src/import_resolver.rs b/src/ephapax-cli/src/import_resolver.rs index 6bc83e80..60c1f6b5 100644 --- a/src/ephapax-cli/src/import_resolver.rs +++ b/src/ephapax-cli/src/import_resolver.rs @@ -151,9 +151,7 @@ fn first_module_declaration(source: &str) -> Option { let rest = rest.trim(); // Take everything up to a whitespace, comma, or comment marker. let end = rest - .find(|c: char| { - !(c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '/') - }) + .find(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '/')) .unwrap_or(rest.len()); let name = &rest[..end]; if !name.is_empty() { @@ -208,15 +206,14 @@ fn visit( message: e.to_string(), })?; - let surface = - parse_surface_module(&source, logical).map_err(|errs| ResolveError::Parse { - path: file_path.clone(), - message: errs - .iter() - .map(|e| format!("{}", e)) - .collect::>() - .join("; "), - })?; + let surface = parse_surface_module(&source, logical).map_err(|errs| ResolveError::Parse { + path: file_path.clone(), + message: errs + .iter() + .map(|e| format!("{}", e)) + .collect::>() + .join("; "), + })?; // Recurse into imports BEFORE inserting this module so that the // post-order visit places dependencies before this module. @@ -226,7 +223,9 @@ fn visit( .map(|i| normalise_path(i.module.as_str())) .collect(); for dep in &deps { - visit(dep, None, base_dir, mod_index, loaded, order, visiting, stack)?; + visit( + dep, None, base_dir, mod_index, loaded, order, visiting, stack, + )?; } loaded.insert( @@ -253,9 +252,9 @@ fn root_module_path_from_source(root_path: &Path) -> Result return Err(e.to_string()), }; @@ -548,8 +556,16 @@ fn compile_file( println!("{} Type check passed", "✓".green()); } - finish_compile(module, &filename, path, output, opt_level, debug, - verify_ownership, verbose) + finish_compile( + module, + &filename, + path, + output, + opt_level, + debug, + verify_ownership, + verbose, + ) } /// Codegen + optional ownership verification + output write. Shared by @@ -564,7 +580,6 @@ fn finish_compile( verify_ownership: bool, verbose: bool, ) -> Result<(), String> { - // Compile to WASM (with or without debug info) let wasm_bytes = if debug { ephapax_wasm::compile_module(&module).map_err(|e| format!("Codegen error: {}", e))? @@ -672,16 +687,11 @@ fn report_ownership_verification(wasm_bytes: &[u8], verbose: bool) -> Result<(), match verify_from_module(wasm_bytes) { Ok(()) => { if verbose { - println!( - "{} typed-wasm L7+L10 verification: clean", - "✓".green() - ); + println!("{} typed-wasm L7+L10 verification: clean", "✓".green()); } Ok(()) } - Err(VerifyError::Parse(e)) => { - Err(format!("verifier could not parse emitted wasm: {}", e)) - } + Err(VerifyError::Parse(e)) => Err(format!("verifier could not parse emitted wasm: {}", e)), Err(VerifyError::Ownership(errs)) => { eprintln!( "{} typed-wasm verification failed: {} violation(s)", diff --git a/src/ephapax-cli/tests/aspect_tests.rs b/src/ephapax-cli/tests/aspect_tests.rs index d133a229..34c87d27 100644 --- a/src/ephapax-cli/tests/aspect_tests.rs +++ b/src/ephapax-cli/tests/aspect_tests.rs @@ -84,9 +84,9 @@ fn security_null_byte_in_source_handled_gracefully() { #[test] fn security_unicode_in_source_no_panic() { let unicode_sources = [ - "42 (* こんにちは *)", // CJK in comment (if comments exist, otherwise parse error OK) - "42", // baseline — must still succeed - "true", // baseline bool + "42 (* こんにちは *)", // CJK in comment (if comments exist, otherwise parse error OK) + "42", // baseline — must still succeed + "true", // baseline bool ]; for source in unicode_sources { // No panic is the requirement; error result is acceptable. @@ -169,14 +169,13 @@ fn performance_batch_let_bindings() { #[test] fn correctness_parse_error_is_informative() { let bad_inputs = [ - "let x =", // truncated let - "@@@", // invalid tokens - "fn(x:) -> x", // missing type annotation - "if then else", // missing condition + "let x =", // truncated let + "@@@", // invalid tokens + "fn(x:) -> x", // missing type annotation + "if then else", // missing condition ]; for source in bad_inputs { - let errors = parse(source) - .expect_err(&format!("`{source}` should fail to parse")); + let errors = parse(source).expect_err(&format!("`{source}` should fail to parse")); for e in &errors { let msg = format!("{e}"); assert!( @@ -217,16 +216,9 @@ fn correctness_type_error_is_informative() { /// The returned type must be serialisable (via Debug) and non-empty. #[test] fn correctness_successful_check_returns_meaningful_type() { - let well_typed = [ - "42", - "true", - "()", - "let x = 1 in x", - "fn(x: I32) -> x", - ]; + let well_typed = ["42", "true", "()", "let x = 1 in x", "fn(x: I32) -> x"]; for source in well_typed { - let ty = check(source) - .unwrap_or_else(|e| panic!("type-check of `{source}` failed: {e}")); + let ty = check(source).unwrap_or_else(|e| panic!("type-check of `{source}` failed: {e}")); let ty_str = format!("{ty:?}"); assert!( !ty_str.trim().is_empty(), diff --git a/src/ephapax-cli/tests/cli_verify_ownership.rs b/src/ephapax-cli/tests/cli_verify_ownership.rs index 4003cddf..f31547fe 100644 --- a/src/ephapax-cli/tests/cli_verify_ownership.rs +++ b/src/ephapax-cli/tests/cli_verify_ownership.rs @@ -27,8 +27,7 @@ fn ephapax_bin() -> String { #[test] fn verify_ownership_clean_module() { let src = tempfile::NamedTempFile::with_suffix(".eph").expect("tempfile"); - std::fs::write(src.path(), "fn add(a: I32, b: I32): I32 = a + b\n") - .expect("write source"); + std::fs::write(src.path(), "fn add(a: I32, b: I32): I32 = a + b\n").expect("write source"); let out = tempfile::NamedTempFile::new().expect("output tempfile"); let result = Command::new(ephapax_bin()) @@ -69,8 +68,7 @@ fn verify_ownership_linear_program() { // satisfying the source-level linear typechecker. The emitted wasm // therefore has param_kinds = [Linear] in the ownership section // and a body that uses local 0 exactly once. - std::fs::write(src.path(), "fn echo(s: String): String = s\n") - .expect("write source"); + std::fs::write(src.path(), "fn echo(s: String): String = s\n").expect("write source"); let out = tempfile::NamedTempFile::new().expect("output tempfile"); let result = Command::new(ephapax_bin()) @@ -106,8 +104,7 @@ fn compile_without_verify_flag_does_not_run_verifier() { // emitted module would hypothetically have issues — the verifier // isn't invoked). let src = tempfile::NamedTempFile::with_suffix(".eph").expect("tempfile"); - std::fs::write(src.path(), "fn add(a: I32, b: I32): I32 = a + b\n") - .expect("write source"); + std::fs::write(src.path(), "fn add(a: I32, b: I32): I32 = a + b\n").expect("write source"); let out = tempfile::NamedTempFile::new().expect("output tempfile"); let result = Command::new(ephapax_bin()) diff --git a/src/ephapax-cli/tests/contract_tests.rs b/src/ephapax-cli/tests/contract_tests.rs index 7ac99227..8f9f6d1f 100644 --- a/src/ephapax-cli/tests/contract_tests.rs +++ b/src/ephapax-cli/tests/contract_tests.rs @@ -218,10 +218,10 @@ fn invariant_nested_linear_bindings_one_unconsumed_rejected() { #[test] fn invariant_type_error_is_typecheck_phase_not_parse_phase() { let ill_typed_programs = [ - "if true then 1 else true", // branch type mismatch - "(fn(x: I32) -> x)(true)", // argument type mismatch - "x", // unbound variable - "let! x = 42 in 0", // unused linear binding + "if true then 1 else true", // branch type mismatch + "(fn(x: I32) -> x)(true)", // argument type mismatch + "x", // unbound variable + "let! x = 42 in 0", // unused linear binding ]; for source in ill_typed_programs { diff --git a/src/ephapax-cli/tests/eph_corpus_gate.rs b/src/ephapax-cli/tests/eph_corpus_gate.rs index f5d9c428..b1ff988f 100644 --- a/src/ephapax-cli/tests/eph_corpus_gate.rs +++ b/src/ephapax-cli/tests/eph_corpus_gate.rs @@ -74,12 +74,7 @@ fn compiles(root: &Path, rel: &str, out_dir: &Path) -> bool { let out = out_dir.join("gate.wasm"); Command::new(ephapax_bin()) .current_dir(root) - .args([ - "compile", - rel, - "-o", - out.to_str().expect("utf8 tmp path"), - ]) + .args(["compile", rel, "-o", out.to_str().expect("utf8 tmp path")]) .output() .map(|o| o.status.success()) .unwrap_or(false) diff --git a/src/ephapax-cli/tests/typed_wasm_seam_conformance.rs b/src/ephapax-cli/tests/typed_wasm_seam_conformance.rs index 89da2d33..cfca4a13 100644 --- a/src/ephapax-cli/tests/typed_wasm_seam_conformance.rs +++ b/src/ephapax-cli/tests/typed_wasm_seam_conformance.rs @@ -83,16 +83,19 @@ fn section_name_constants_agree() { #[test] fn eph_build_decoded_by_tw_parse() { let entries = vec![ - eph_entry(0, &[1], 0), // (Linear) -> Unrestricted - eph_entry(7, &[0, 2, 3], 1), // (Unrestricted, SharedBorrow, ExclBorrow) -> Linear - eph_entry(42, &[], 0), // nullary -> Unrestricted + eph_entry(0, &[1], 0), // (Linear) -> Unrestricted + eph_entry(7, &[0, 2, 3], 1), // (Unrestricted, SharedBorrow, ExclBorrow) -> Linear + eph_entry(42, &[], 0), // nullary -> Unrestricted ]; let payload = eph::build_ownership_section_payload(&entries); let decoded = tw::parse_ownership_section_payload(&payload); let want: Vec<_> = entries.iter().map(eph_norm).collect(); let got: Vec<_> = decoded.iter().map(tw_norm).collect(); - assert_eq!(got, want, "typed-wasm decoded ephapax-built payload differently"); + assert_eq!( + got, want, + "typed-wasm decoded ephapax-built payload differently" + ); } /// typed-wasm encodes; ephapax decodes. Symmetric agreement. @@ -108,7 +111,10 @@ fn tw_build_decoded_by_eph_parse() { let want: Vec<_> = entries.iter().map(tw_norm).collect(); let got: Vec<_> = decoded.iter().map(eph_norm).collect(); - assert_eq!(got, want, "ephapax decoded typed-wasm-built payload differently"); + assert_eq!( + got, want, + "ephapax decoded typed-wasm-built payload differently" + ); } /// Exhaustive cross-codec round-trip over every ownership kind byte and a @@ -127,14 +133,22 @@ fn cross_codec_exhaustive_roundtrip() { let payload = eph::build_ownership_section_payload(std::slice::from_ref(&e)); let back = tw::parse_ownership_section_payload(&payload); assert_eq!(back.len(), 1); - assert_eq!(tw_norm(&back[0]), eph_norm(&e), "eph->tw mismatch n={n_params} ret={ret}"); + assert_eq!( + tw_norm(&back[0]), + eph_norm(&e), + "eph->tw mismatch n={n_params} ret={ret}" + ); // tw -> eph -> compare let t = tw_entry(func_idx, ¶ms, ret); let payload2 = tw::build_ownership_section_payload(std::slice::from_ref(&t)); let back2 = eph::parse_ownership_section_payload(&payload2); assert_eq!(back2.len(), 1); - assert_eq!(eph_norm(&back2[0]), tw_norm(&t), "tw->eph mismatch n={n_params} ret={ret}"); + assert_eq!( + eph_norm(&back2[0]), + tw_norm(&t), + "tw->eph mismatch n={n_params} ret={ret}" + ); } } } @@ -228,7 +242,11 @@ fn typed_wasm_verifies_ephapax_access_sites() { .expect("typedwasm.regions section present"); let regions = tw::section::parse_regions_section_payload(®ions_payload) .expect("typed-wasm rejected ephapax's regions payload"); - assert_eq!(regions.len(), 1, "expected the single ephapax.string region"); + assert_eq!( + regions.len(), + 1, + "expected the single ephapax.string region" + ); let access_payload = extract_custom_section(&wasm, "typedwasm.access-sites") .expect("typedwasm.access-sites section present"); diff --git a/src/ephapax-cli/tests/v2_grammar_phase_e.rs b/src/ephapax-cli/tests/v2_grammar_phase_e.rs index 8432a273..7797eb52 100644 --- a/src/ephapax-cli/tests/v2_grammar_phase_e.rs +++ b/src/ephapax-cli/tests/v2_grammar_phase_e.rs @@ -28,8 +28,7 @@ const LET_PAIR: &str = include_str!(concat!( #[test] fn annotation_on_fn_decl_parses() { let source = "module test\n@tail_recursive\nfn run(): I32 = 0\n"; - let _ = parse_surface_module(source, "annotation-test") - .expect("@annotation prefix must parse"); + let _ = parse_surface_module(source, "annotation-test").expect("@annotation prefix must parse"); } #[test] diff --git a/src/ephapax-cli/tests/v2_grammar_phase_k_qualified.rs b/src/ephapax-cli/tests/v2_grammar_phase_k_qualified.rs index e528f8a9..4ee5f777 100644 --- a/src/ephapax-cli/tests/v2_grammar_phase_k_qualified.rs +++ b/src/ephapax-cli/tests/v2_grammar_phase_k_qualified.rs @@ -51,10 +51,12 @@ fn qualified_module_member_access_compiles_end_to_end() { fn unknown_qualifier_is_a_clean_error() { let dir = tempfile::tempdir().expect("temp dir"); let app = dir.path().join("app.eph"); - std::fs::write(&app, "module app\nimport lib\nfn t(): I32 = nope.inc(1)\n") - .expect("write app"); - std::fs::write(dir.path().join("lib.eph"), "module lib\npub fn inc(n: I32): I32 = n + 1\n") - .expect("write lib"); + std::fs::write(&app, "module app\nimport lib\nfn t(): I32 = nope.inc(1)\n").expect("write app"); + std::fs::write( + dir.path().join("lib.eph"), + "module lib\npub fn inc(n: I32): I32 = n + 1\n", + ) + .expect("write lib"); let out = tempfile::NamedTempFile::new().expect("temp file"); let output = Command::new(ephapax_bin()) .arg("compile-eph") @@ -63,7 +65,10 @@ fn unknown_qualifier_is_a_clean_error() { .arg(out.path()) .output() .expect("ephapax must run"); - assert!(!output.status.success(), "unknown qualifier must fail to compile"); + assert!( + !output.status.success(), + "unknown qualifier must fail to compile" + ); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("not an imported module"), diff --git a/src/ephapax-interp/src/lib.rs b/src/ephapax-interp/src/lib.rs index bc944f84..7c4e8457 100644 --- a/src/ephapax-interp/src/lib.rs +++ b/src/ephapax-interp/src/lib.rs @@ -214,7 +214,10 @@ impl Value { param: Box::new(param_ty.clone()), ret: Box::new(Ty::Base(BaseTy::Unit)), // Unknown without evaluation }, - Value::Borrow(inner) => Ty::Borrow { inner: Box::new(inner.to_type()), mutable: false }, + Value::Borrow(inner) => Ty::Borrow { + inner: Box::new(inner.to_type()), + mutable: false, + }, } } } @@ -554,16 +557,12 @@ impl Interpreter { } } - ExprKind::Perform { .. } => { - Err(RuntimeError::Unimplemented( - "effect perform not yet implemented in interpreter".into(), - )) - } - ExprKind::Handle { .. } => { - Err(RuntimeError::Unimplemented( - "effect handle not yet implemented in interpreter".into(), - )) - } + ExprKind::Perform { .. } => Err(RuntimeError::Unimplemented( + "effect perform not yet implemented in interpreter".into(), + )), + ExprKind::Handle { .. } => Err(RuntimeError::Unimplemented( + "effect handle not yet implemented in interpreter".into(), + )), ExprKind::Match { scrutinee, arms } => self.eval_match(scrutinee, arms), } } @@ -908,11 +907,7 @@ impl Interpreter { /// and restores prior bindings. No arms matching is a runtime /// error — the typechecker's exhaustiveness pass should prevent /// this when the module is well-typed. - fn eval_match( - &mut self, - scrutinee: &Expr, - arms: &[MatchArm], - ) -> Result { + fn eval_match(&mut self, scrutinee: &Expr, arms: &[MatchArm]) -> Result { let scrut_val = self.eval(scrutinee)?; for arm in arms { let mut new_bindings: Vec<(Var, Value)> = Vec::new(); @@ -1105,9 +1100,13 @@ impl Interpreter { match prev { Some(v) => { self.env.bindings.insert(left_var.clone(), v); - if let Some(c) = prev_c { self.env.consumed.insert(left_var.clone(), c); } + if let Some(c) = prev_c { + self.env.consumed.insert(left_var.clone(), c); + } + } + None => { + self.env.remove(left_var); } - None => { self.env.remove(left_var); } } Ok(result) } @@ -1119,9 +1118,13 @@ impl Interpreter { match prev { Some(v) => { self.env.bindings.insert(right_var.clone(), v); - if let Some(c) = prev_c { self.env.consumed.insert(right_var.clone(), c); } + if let Some(c) = prev_c { + self.env.consumed.insert(right_var.clone(), c); + } + } + None => { + self.env.remove(right_var); } - None => { self.env.remove(right_var); } } Ok(result) } @@ -1664,24 +1667,31 @@ mod tests { }); let expr = dummy_expr(ExprKind::Match { scrutinee: Box::new(scrut), - arms: vec![MatchArm { - pattern: P::Constructor { - ctor: "Some".into(), - args: vec![P::Var("v".into())], + arms: vec![ + MatchArm { + pattern: P::Constructor { + ctor: "Some".into(), + args: vec![P::Var("v".into())], + }, + guard: None, + body: dummy_expr(ExprKind::Var("v".into())), }, - guard: None, - body: dummy_expr(ExprKind::Var("v".into())), - }, MatchArm { - pattern: P::Wildcard, - guard: None, - body: lit_i32_expr(0), - }], + MatchArm { + pattern: P::Wildcard, + guard: None, + body: lit_i32_expr(0), + }, + ], }); let result = interp.eval(&expr).expect("match should evaluate"); assert!(matches!(result, Value::I32(7))); let outer = interp.env.get(&Var::from("v")).cloned().unwrap(); - assert!(matches!(outer, Value::I32(100)), "outer v leaked: {:?}", outer); + assert!( + matches!(outer, Value::I32(100)), + "outer v leaked: {:?}", + outer + ); } /// Multi-field constructor: `Pair(a, b)` of arity 2 lives in a @@ -1742,7 +1752,11 @@ mod tests { }], }); let err = interp.eval(&expr).unwrap_err(); - assert!(matches!(err, RuntimeError::PatternMatchFailed), "got {:?}", err); + assert!( + matches!(err, RuntimeError::PatternMatchFailed), + "got {:?}", + err + ); // suppress unused-import warning for Visibility (held for symmetry // with the typing test module pattern). let _ = Visibility::Private; @@ -1820,7 +1834,9 @@ mod tests { }, ], }); - let result = interp.eval(&expr).expect("guard fall-through should evaluate"); + let result = interp + .eval(&expr) + .expect("guard fall-through should evaluate"); assert!(matches!(result, Value::I32(0)), "got {:?}", result); } diff --git a/src/ephapax-ir/src/lib.rs b/src/ephapax-ir/src/lib.rs index 47c2c44a..f3c0a69e 100644 --- a/src/ephapax-ir/src/lib.rs +++ b/src/ephapax-ir/src/lib.rs @@ -7,7 +7,9 @@ //! //! Provides a small, stable S-expression encoding for Ephapax AST nodes. -use ephapax_syntax::{BaseTy, BinOp, Decl, Expr, ExprKind, Literal, Module, Ty, UnaryOp, Visibility}; +use ephapax_syntax::{ + BaseTy, BinOp, Decl, Expr, ExprKind, Literal, Module, Ty, UnaryOp, Visibility, +}; use smol_str::SmolStr; use std::fmt; use thiserror::Error; @@ -267,7 +269,11 @@ fn decl_to_sexpr(decl: &Decl) -> SExpr { ty_to_sexpr(ret_ty), expr_to_sexpr(body), ]), - Decl::Type { name, visibility: _, ty } => SExpr::List(vec![ + Decl::Type { + name, + visibility: _, + ty, + } => SExpr::List(vec![ SExpr::Atom("type".into()), SExpr::Atom(escape_atom(name)), ty_to_sexpr(ty), @@ -275,7 +281,9 @@ fn decl_to_sexpr(decl: &Decl) -> SExpr { Decl::Const { name, ty, value } => SExpr::List(vec![ SExpr::Atom("const".into()), SExpr::Atom(escape_atom(name)), - ty.as_ref().map(ty_to_sexpr).unwrap_or(SExpr::Atom("_".into())), + ty.as_ref() + .map(ty_to_sexpr) + .unwrap_or(SExpr::Atom("_".into())), expr_to_sexpr(value), ]), // Render `extern "abi" { ... }` as a tagged s-expr so the IR @@ -563,10 +571,7 @@ pub fn expr_to_sexpr(expr: &Expr) -> SExpr { SExpr::List(elems) } ExprKind::Perform { op, args } => { - let mut elems = vec![ - SExpr::Atom("perform".into()), - SExpr::Atom(escape_atom(op)), - ]; + let mut elems = vec![SExpr::Atom("perform".into()), SExpr::Atom(escape_atom(op))]; elems.extend(args.iter().map(expr_to_sexpr)); SExpr::List(elems) } @@ -586,13 +591,9 @@ pub fn expr_to_sexpr(expr: &Expr) -> SExpr { // ephapax#61 so the structured `match` survives IR round-trip. // Guards render as `(arm (guard ) )`. ExprKind::Match { scrutinee, arms } => { - let mut elems = - vec![SExpr::Atom("match".into()), expr_to_sexpr(scrutinee)]; + let mut elems = vec![SExpr::Atom("match".into()), expr_to_sexpr(scrutinee)]; for arm in arms { - let mut arm_elems = vec![ - SExpr::Atom("arm".into()), - pattern_to_sexpr(&arm.pattern), - ]; + let mut arm_elems = vec![SExpr::Atom("arm".into()), pattern_to_sexpr(&arm.pattern)]; if let Some(guard) = &arm.guard { arm_elems.push(SExpr::List(vec![ SExpr::Atom("guard".into()), @@ -612,13 +613,10 @@ fn pattern_to_sexpr(pattern: &ephapax_syntax::Pattern) -> SExpr { match pattern { Pattern::Wildcard => SExpr::Atom("_".into()), Pattern::Unit => SExpr::List(vec![SExpr::Atom("unit".into())]), - Pattern::Var(v) => SExpr::List(vec![ - SExpr::Atom("var".into()), - SExpr::Atom(escape_atom(v)), - ]), - Pattern::Literal(lit) => { - SExpr::List(vec![SExpr::Atom("lit".into()), lit_to_sexpr(lit)]) + Pattern::Var(v) => { + SExpr::List(vec![SExpr::Atom("var".into()), SExpr::Atom(escape_atom(v))]) } + Pattern::Literal(lit) => SExpr::List(vec![SExpr::Atom("lit".into()), lit_to_sexpr(lit)]), Pattern::Pair(l, r) => SExpr::List(vec![ SExpr::Atom("pair".into()), pattern_to_sexpr(l), @@ -630,10 +628,7 @@ fn pattern_to_sexpr(pattern: &ephapax_syntax::Pattern) -> SExpr { SExpr::List(elems) } Pattern::Constructor { ctor, args } => { - let mut elems = vec![ - SExpr::Atom("ctor".into()), - SExpr::Atom(escape_atom(ctor)), - ]; + let mut elems = vec![SExpr::Atom("ctor".into()), SExpr::Atom(escape_atom(ctor))]; elems.extend(args.iter().map(pattern_to_sexpr)); SExpr::List(elems) } diff --git a/src/ephapax-lexer/src/lib.rs b/src/ephapax-lexer/src/lib.rs index 195f7fea..ff795bcd 100644 --- a/src/ephapax-lexer/src/lib.rs +++ b/src/ephapax-lexer/src/lib.rs @@ -854,8 +854,14 @@ mod property_tests { fn p2p_empty_string_no_content_tokens_no_errors() { for _ in 0..20 { let (tokens, errors) = tokenize_no_eof(""); - assert!(tokens.is_empty(), "P2P empty: empty source must produce no content tokens"); - assert!(errors.is_empty(), "P2P empty: empty source must produce no errors"); + assert!( + tokens.is_empty(), + "P2P empty: empty source must produce no content tokens" + ); + assert!( + errors.is_empty(), + "P2P empty: empty source must produce no errors" + ); } } @@ -864,9 +870,7 @@ mod property_tests { /// The lexer skips whitespace; pure whitespace inputs should yield no content tokens. #[test] fn p2p_whitespace_only_no_content_tokens() { - let whitespace_inputs = [ - " ", " ", "\t", "\n", "\r\n", " \t\n ", "\n\n\n", - ]; + let whitespace_inputs = [" ", " ", "\t", "\n", "\r\n", " \t\n ", "\n\n\n"]; for source in &whitespace_inputs { let tokens: Vec = Lexer::new(source) .filter_map(|r| r.ok()) @@ -888,19 +892,23 @@ mod property_tests { #[test] fn p2p_integer_literals_tokenise_to_one_token() { let inputs: Vec<(&str, fn(&TokenKind) -> bool)> = vec![ - ("0", |t| matches!(t, TokenKind::Integer(_))), - ("1", |t| matches!(t, TokenKind::Integer(_))), - ("42", |t| matches!(t, TokenKind::Integer(_))), - ("100", |t| matches!(t, TokenKind::Integer(_))), - ("999", |t| matches!(t, TokenKind::Integer(_))), + ("0", |t| matches!(t, TokenKind::Integer(_))), + ("1", |t| matches!(t, TokenKind::Integer(_))), + ("42", |t| matches!(t, TokenKind::Integer(_))), + ("100", |t| matches!(t, TokenKind::Integer(_))), + ("999", |t| matches!(t, TokenKind::Integer(_))), ("2147483647", |t| matches!(t, TokenKind::Integer(_))), ]; for (source, predicate) in &inputs { let (tokens, errors) = tokenize_no_eof(source); - assert!(errors.is_empty(), "P2P int[{source}]: unexpected lex errors"); + assert!( + errors.is_empty(), + "P2P int[{source}]: unexpected lex errors" + ); assert_eq!( - tokens.len(), 1, + tokens.len(), + 1, "P2P int[{source}]: expected exactly one content token" ); assert!( @@ -931,8 +939,15 @@ mod property_tests { ); // Must contain Ident tokens for 'add', 'x', 'y' - let idents: Vec<&SmolStr> = tokens.iter() - .filter_map(|t| if let TokenKind::Ident(s) = &t.kind { Some(s) } else { None }) + let idents: Vec<&SmolStr> = tokens + .iter() + .filter_map(|t| { + if let TokenKind::Ident(s) = &t.kind { + Some(s) + } else { + None + } + }) .collect(); assert!( idents.iter().any(|s| s.as_str() == "add"), @@ -954,7 +969,7 @@ mod property_tests { #[test] fn e2e_letlin_distinguished_from_let() { let source_letlin = "let! x = 1 in x"; - let source_let = "let x = 1"; + let source_let = "let x = 1"; let (tokens_lin, errors_lin) = tokenize_no_eof(source_letlin); let (tokens_let, errors_let) = tokenize_no_eof(source_let); @@ -963,7 +978,9 @@ mod property_tests { assert!(errors_let.is_empty(), "E2E let: no lex errors"); assert!( - tokens_lin.iter().any(|t| matches!(t.kind, TokenKind::LetBang)), + tokens_lin + .iter() + .any(|t| matches!(t.kind, TokenKind::LetBang)), "E2E let!: must contain LetLin token" ); assert!( @@ -971,7 +988,9 @@ mod property_tests { "E2E let: must contain Let token" ); assert!( - !tokens_let.iter().any(|t| matches!(t.kind, TokenKind::LetBang)), + !tokens_let + .iter() + .any(|t| matches!(t.kind, TokenKind::LetBang)), "E2E let: plain let must NOT contain LetLin token" ); } @@ -1038,7 +1057,8 @@ mod property_tests { .collect(); assert_eq!( - batch_tokens.len(), iter_tokens.len(), + batch_tokens.len(), + iter_tokens.len(), "Aspect consistency[{source:?}]: batch and iterator must yield same token count" ); diff --git a/src/ephapax-lsp/src/main.rs b/src/ephapax-lsp/src/main.rs index 0f26c3d9..0169cd26 100644 --- a/src/ephapax-lsp/src/main.rs +++ b/src/ephapax-lsp/src/main.rs @@ -173,7 +173,11 @@ impl Backend { if let Some(module) = &state.module { for d in &module.decls { if let Decl::Fn { - name, body, params, type_params: _, .. + name, + body, + params, + type_params: _, + .. } = d { // Check if word matches a parameter @@ -479,9 +483,7 @@ impl LanguageServer for Backend { if let Some(state) = docs.get(uri) { for decl in &state.declarations { let kind = match decl.kind { - DeclKind::Function | DeclKind::ExternFn => { - CompletionItemKind::FUNCTION - } + DeclKind::Function | DeclKind::ExternFn => CompletionItemKind::FUNCTION, DeclKind::TypeAlias | DeclKind::ExternType | DeclKind::Data => { CompletionItemKind::TYPE_PARAMETER } @@ -546,7 +548,11 @@ fn extract_declarations(module: &Module, _source: &str) -> Vec { return_type: Some(format_ty(ret_ty)), }); } - Decl::Type { name, visibility: _, ty } => out.push(DeclInfo { + Decl::Type { + name, + visibility: _, + ty, + } => out.push(DeclInfo { name: name.to_string(), kind: DeclKind::TypeAlias, span: Span::dummy(), @@ -561,7 +567,9 @@ fn extract_declarations(module: &Module, _source: &str) -> Vec { signature: format!( "let {} {}= ...", name, - ty.as_ref().map(|t| format!(": {} ", format_ty(t))).unwrap_or_default() + ty.as_ref() + .map(|t| format!(": {} ", format_ty(t))) + .unwrap_or_default() ), params: Vec::new(), return_type: ty.as_ref().map(|t| format_ty(t)), @@ -702,7 +710,11 @@ fn format_ty(ty: &Ty) -> String { Ty::Ref { inner, .. } => format!("Ref({})", format_ty(inner)), Ty::String(region) => format!("String@{}", region), Ty::Region { name, inner } => format!("Region({}, {})", name, format_ty(inner)), - Ty::Borrow { inner, mutable } => format!("&{}{}", if *mutable { "mut " } else { "" }, format_ty(inner)), + Ty::Borrow { inner, mutable } => format!( + "&{}{}", + if *mutable { "mut " } else { "" }, + format_ty(inner) + ), Ty::Var(name) => name.to_string(), Ty::ForAll { var, body } => format!("forall {}. {}", var, format_ty(body)), Ty::Unif(id) => format!("?{}", id), diff --git a/src/ephapax-meta/src/lib.rs b/src/ephapax-meta/src/lib.rs index 29310bff..02c04403 100644 --- a/src/ephapax-meta/src/lib.rs +++ b/src/ephapax-meta/src/lib.rs @@ -73,7 +73,10 @@ impl std::fmt::Display for MetaError { match self { MetaError::CannotReify(t) => write!(f, "cannot reify a {t} value into syntax"), MetaError::LinearSplice(t) => { - write!(f, "refused to reify linear {t} value (would duplicate a resource)") + write!( + f, + "refused to reify linear {t} value (would duplicate a resource)" + ) } MetaError::Decode(e) => write!(f, "decode error: {e}"), MetaError::Eval(e) => write!(f, "eval error: {e}"), @@ -131,7 +134,9 @@ pub struct ReflectiveInterp { impl ReflectiveInterp { /// A fresh reflective interpreter. pub fn new() -> Self { - Self { inner: Interpreter::new() } + Self { + inner: Interpreter::new(), + } } /// Quote an expression to data. @@ -268,7 +273,9 @@ mod tests { let stager = WasmStager::new(); let wasm = stager.compile_stage(&mul(6, 7)).expect("compile stage"); assert!(wasm.starts_with(b"\0asm"), "valid wasm magic"); - stager.run_stage(&mul(6, 7)).expect("staged run must not trap"); + stager + .run_stage(&mul(6, 7)) + .expect("staged run must not trap"); } #[test] diff --git a/src/ephapax-package/src/manifest.rs b/src/ephapax-package/src/manifest.rs index af2c8951..be8622b2 100644 --- a/src/ephapax-package/src/manifest.rs +++ b/src/ephapax-package/src/manifest.rs @@ -197,7 +197,12 @@ fn is_valid_package_name(name: &str) -> bool { // Must start with letter // invariant: we already checked !is_empty(), so chars().next() cannot fail - if !name.chars().next().expect("invariant: name is not empty").is_ascii_lowercase() { + if !name + .chars() + .next() + .expect("invariant: name is not empty") + .is_ascii_lowercase() + { return false; } diff --git a/src/ephapax-parser/src/lib.rs b/src/ephapax-parser/src/lib.rs index be609a56..db7f9b1b 100644 --- a/src/ephapax-parser/src/lib.rs +++ b/src/ephapax-parser/src/lib.rs @@ -279,11 +279,10 @@ fn parse_extern_item(pair: pest::iterators::Pair) -> Result) -> Result) -> Result { @@ -600,7 +603,10 @@ fn parse_type_atom(pair: pest::iterators::Pair) -> Result (false, first) }; let inner_ty = parse_type_atom(ty_pair)?; - Ok(Ty::Borrow { inner: Box::new(inner_ty), mutable }) + Ok(Ty::Borrow { + inner: Box::new(inner_ty), + mutable, + }) } Rule::list_ty => { let elem_ty = parse_type( @@ -654,7 +660,10 @@ fn parse_type_atom(pair: pest::iterators::Pair) -> Result if parts.next().is_some() { // Has arguments — not supported in core parser return Err(ParseError::Syntax { - message: format!("Parameterized type '{}(...)' requires the surface parser", name_str), + message: format!( + "Parameterized type '{}(...)' requires the surface parser", + name_str + ), span: span_from_pair(&name), }); } @@ -755,7 +764,10 @@ fn parse_seq_expr_core(pair: pest::iterators::Pair) -> Result) -> Result ResumeMode::Once, }); } else { - resume_mode = Some(ResumeMode::Once); // default + resume_mode = Some(ResumeMode::Once); + // default } } Rule::identifier => { @@ -1290,8 +1303,7 @@ fn parse_handle_expr(pair: pest::iterators::Pair) -> Result {} @@ -1591,10 +1603,7 @@ fn parse_postfix_expr(pair: pest::iterators::Pair) -> Result) -> Result) -> Result v | None => 0 end"; + let source = "fn unwrap_or_zero(x: I32): I32 = match x of | Some(v) => v | None => 0 end"; let module = parse_module(source, "").expect("should parse"); assert_eq!(module.decls.len(), 1); let Decl::Fn { body, .. } = &module.decls[0] else { @@ -2342,7 +2352,9 @@ mod tests { Decl::Extern { abi, items } => { assert_eq!(abi, "gossamer"); assert_eq!(items.len(), 3); - assert!(matches!(&items[0], ExternItem::Type { name } if name.as_str() == "Window")); + assert!( + matches!(&items[0], ExternItem::Type { name } if name.as_str() == "Window") + ); if let ExternItem::Fn { name, params, .. } = &items[1] { assert_eq!(name.as_str(), "window_open"); assert_eq!(params.len(), 2); @@ -2420,10 +2432,7 @@ mod tests { constructors, } => { assert_eq!(name.as_str(), "Result"); - assert_eq!( - type_params, - &vec![SmolStr::new("a"), SmolStr::new("e")] - ); + assert_eq!(type_params, &vec![SmolStr::new("a"), SmolStr::new("e")]); assert_eq!(constructors.len(), 2); assert_eq!(constructors[0].name.as_str(), "Ok"); assert!(matches!(&constructors[0].fields[0], Ty::Var(v) if v == "a")); @@ -2442,7 +2451,9 @@ mod tests { let module = parse_module(source, "test").expect("should parse"); assert_eq!(module.decls.len(), 1); match &module.decls[0] { - Decl::Fn { name, visibility, .. } => { + Decl::Fn { + name, visibility, .. + } => { assert_eq!(name.as_str(), "double"); assert_eq!(*visibility, Visibility::Public); } @@ -2467,7 +2478,13 @@ mod tests { let source = r#"fn identity(x: T): T = x"#; let module = parse_module(source, "test").expect("should parse"); match &module.decls[0] { - Decl::Fn { name, type_params, params, ret_ty, .. } => { + Decl::Fn { + name, + type_params, + params, + ret_ty, + .. + } => { assert_eq!(name.as_str(), "identity"); assert_eq!(type_params.len(), 1); assert_eq!(type_params[0].as_str(), "T"); @@ -2498,7 +2515,11 @@ mod tests { let source = r#"pub fn identity(x: T): T = x"#; let module = parse_module(source, "test").expect("should parse"); match &module.decls[0] { - Decl::Fn { visibility, type_params, .. } => { + Decl::Fn { + visibility, + type_params, + .. + } => { assert_eq!(*visibility, Visibility::Public); assert_eq!(type_params.len(), 1); } @@ -2538,7 +2559,9 @@ mod tests { let source = r#"pub type Alias = I32"#; let module = parse_module(source, "test").expect("should parse"); match &module.decls[0] { - Decl::Type { name, visibility, .. } => { + Decl::Type { + name, visibility, .. + } => { assert_eq!(name.as_str(), "Alias"); assert_eq!(*visibility, Visibility::Public); } @@ -2592,7 +2615,11 @@ mod tests { | ask(k) => k end"#; let result = parse(source); - assert!(result.is_ok(), "should parse handle with op: {:?}", result.err()); + assert!( + result.is_ok(), + "should parse handle with op: {:?}", + result.err() + ); match result.unwrap().kind { ExprKind::Handle { clauses, .. } => { assert_eq!(clauses.len(), 2); diff --git a/src/ephapax-query/src/lib.rs b/src/ephapax-query/src/lib.rs index aa153068..aa545726 100644 --- a/src/ephapax-query/src/lib.rs +++ b/src/ephapax-query/src/lib.rs @@ -124,11 +124,15 @@ impl QueryDb { } self.revision.0 += 1; let changed_at = self.revision; - self.inputs.insert(id.to_string(), Input { text, changed_at }); + self.inputs + .insert(id.to_string(), Input { text, changed_at }); } fn input_changed_at(&self, id: &str) -> Revision { - self.inputs.get(id).map(|i| i.changed_at).unwrap_or_default() + self.inputs + .get(id) + .map(|i| i.changed_at) + .unwrap_or_default() } /// Parse query. Memoises `parse_module`; depends only on the source @@ -143,7 +147,11 @@ impl QueryDb { return value; } } - let text = self.inputs.get(id).map(|i| i.text.clone()).unwrap_or_default(); + let text = self + .inputs + .get(id) + .map(|i| i.text.clone()) + .unwrap_or_default(); let new = parse_module(&text, id); self.recomputes += 1; let rev = self.revision; @@ -153,7 +161,11 @@ impl QueryDb { }; self.parsed.insert( id.to_string(), - Memo { value: new.clone(), changed_at, verified_at: rev }, + Memo { + value: new.clone(), + changed_at, + verified_at: rev, + }, ); new } @@ -161,7 +173,11 @@ impl QueryDb { /// Type-check query. Memoises `type_check_module`; depends on `parsed`. pub fn typed(&mut self, id: &str) -> TypedResult { let parsed = self.parsed(id); - let dep = self.parsed.get(id).map(|m| m.changed_at).unwrap_or_default(); + let dep = self + .parsed + .get(id) + .map(|m| m.changed_at) + .unwrap_or_default(); let rev = self.revision; if let Some(m) = self.typed.get_mut(id) { if dep <= m.verified_at { @@ -184,7 +200,11 @@ impl QueryDb { }; self.typed.insert( id.to_string(), - Memo { value: new.clone(), changed_at, verified_at: rev }, + Memo { + value: new.clone(), + changed_at, + verified_at: rev, + }, ); new } @@ -194,7 +214,11 @@ impl QueryDb { pub fn wasm(&mut self, id: &str) -> WasmResult { let parsed = self.parsed(id); let typed = self.typed(id); - let dep_parsed = self.parsed.get(id).map(|m| m.changed_at).unwrap_or_default(); + let dep_parsed = self + .parsed + .get(id) + .map(|m| m.changed_at) + .unwrap_or_default(); let dep_typed = self.typed.get(id).map(|m| m.changed_at).unwrap_or_default(); let dep = dep_parsed.max(dep_typed); let rev = self.revision; @@ -218,7 +242,11 @@ impl QueryDb { }; self.wasm.insert( id.to_string(), - Memo { value: new.clone(), changed_at, verified_at: rev }, + Memo { + value: new.clone(), + changed_at, + verified_at: rev, + }, ); new } @@ -240,7 +268,11 @@ mod tests { // Re-demand with no edit: nothing recomputes. let _ = db.wasm("m"); - assert_eq!(db.recompute_count(), n, "re-demand with no edit must skip everything"); + assert_eq!( + db.recompute_count(), + n, + "re-demand with no edit must skip everything" + ); } #[test] @@ -252,7 +284,11 @@ mod tests { // Writing the exact same text must not bump the revision. db.set_source_text("m", ADD); let _ = db.wasm("m"); - assert_eq!(db.recompute_count(), n, "identical set_source_text must not invalidate"); + assert_eq!( + db.recompute_count(), + n, + "identical set_source_text must not invalidate" + ); } #[test] diff --git a/src/ephapax-repl/src/lib.rs b/src/ephapax-repl/src/lib.rs index f71e6347..4f7fb745 100644 --- a/src/ephapax-repl/src/lib.rs +++ b/src/ephapax-repl/src/lib.rs @@ -414,7 +414,11 @@ fn format_type(ty: &Ty) -> String { Ty::Fun { param, ret } => format!("{} -> {}", format_type(param), format_type(ret)), Ty::Prod { left, right } => format!("({}, {})", format_type(left), format_type(right)), Ty::Sum { left, right } => format!("{} + {}", format_type(left), format_type(right)), - Ty::Borrow { inner, mutable } => format!("&{}{}", if *mutable { "mut " } else { "" }, format_type(inner)), + Ty::Borrow { inner, mutable } => format!( + "&{}{}", + if *mutable { "mut " } else { "" }, + format_type(inner) + ), Ty::List(elem_ty) => format!("[{}]", format_type(elem_ty)), Ty::Tuple(elem_types) => { let types_str = elem_types @@ -435,7 +439,12 @@ fn format_type(ty: &Ty) -> String { effects, } => { let effs = effects.join(" + "); - format!("{} -> {} with {}", format_type(param), format_type(ret), effs) + format!( + "{} -> {} with {}", + format_type(param), + format_type(ret), + effs + ) } } } diff --git a/src/ephapax-syntax/src/lib.rs b/src/ephapax-syntax/src/lib.rs index 6218e2b8..edc3391b 100644 --- a/src/ephapax-syntax/src/lib.rs +++ b/src/ephapax-syntax/src/lib.rs @@ -202,14 +202,21 @@ impl Ty { inner: Box::new(inner.subst_var(var, replacement)), mutable: *mutable, }, - Ty::Effectful { param, ret, effects } => Ty::Effectful { + Ty::Effectful { + param, + ret, + effects, + } => Ty::Effectful { param: Box::new(param.subst_var(var, replacement)), ret: Box::new(ret.subst_var(var, replacement)), effects: effects.clone(), }, Ty::List(inner) => Ty::List(Box::new(inner.subst_var(var, replacement))), Ty::Tuple(elems) => Ty::Tuple( - elems.iter().map(|t| t.subst_var(var, replacement)).collect(), + elems + .iter() + .map(|t| t.subst_var(var, replacement)) + .collect(), ), } } @@ -229,9 +236,7 @@ impl Ty { | Ty::Borrow { inner, .. } | Ty::List(inner) | Ty::ForAll { body: inner, .. } => inner.contains_unif(id), - Ty::Effectful { param, ret, .. } => { - param.contains_unif(id) || ret.contains_unif(id) - } + Ty::Effectful { param, ret, .. } => param.contains_unif(id) || ret.contains_unif(id), Ty::Tuple(elems) => elems.iter().any(|t| t.contains_unif(id)), } } @@ -275,15 +280,17 @@ impl Ty { var: var.clone(), body: Box::new(body.resolve(solutions)), }, - Ty::Effectful { param, ret, effects } => Ty::Effectful { + Ty::Effectful { + param, + ret, + effects, + } => Ty::Effectful { param: Box::new(param.resolve(solutions)), ret: Box::new(ret.resolve(solutions)), effects: effects.clone(), }, Ty::List(inner) => Ty::List(Box::new(inner.resolve(solutions))), - Ty::Tuple(elems) => Ty::Tuple( - elems.iter().map(|t| t.resolve(solutions)).collect(), - ), + Ty::Tuple(elems) => Ty::Tuple(elems.iter().map(|t| t.resolve(solutions)).collect()), } } } @@ -355,10 +362,7 @@ pub enum Pattern { Tuple(Vec), /// Constructor pattern: `Some(x)`, `None`, `Ok((a, b))` (added /// ephapax#61 for core `match` parsing). - Constructor { - ctor: SmolStr, - args: Vec, - }, + Constructor { ctor: SmolStr, args: Vec }, } impl Pattern { @@ -654,10 +658,7 @@ pub enum Decl { /// Extern types are opaque to the type checker (no constructors, /// no destructors known); extern fns get an ambient binding with /// the declared type. - Extern { - abi: String, - items: Vec, - }, + Extern { abi: String, items: Vec }, /// Algebraic data type declaration: `data Name(a, b) = C1 | C2(T) | ...`. /// @@ -838,8 +839,14 @@ mod tests { name: "r".into(), inner: Box::new(unr_inner), }; - assert!(lin_region.is_linear(), "Coq bridge: TRegion _ (linear) → recurse"); - assert!(!unr_region.is_linear(), "Coq bridge: TRegion _ (unrestricted) → recurse"); + assert!( + lin_region.is_linear(), + "Coq bridge: TRegion _ (linear) → recurse" + ); + assert!( + !unr_region.is_linear(), + "Coq bridge: TRegion _ (unrestricted) → recurse" + ); } #[test] @@ -901,7 +908,10 @@ mod tests { body: boxed_base(), }; assert!(lin_body.is_linear(), "ForAll over a linear body is linear"); - assert!(!unr_body.is_linear(), "ForAll over an unrestricted body is unrestricted"); + assert!( + !unr_body.is_linear(), + "ForAll over an unrestricted body is unrestricted" + ); } #[test] @@ -912,8 +922,6 @@ mod tests { assert!(!Ty::Var("T".into()).is_linear()); assert!(!Ty::Unif(0).is_linear()); assert!(!Ty::List(Box::new(Ty::String("r".into()))).is_linear()); - assert!( - !Ty::Tuple(vec![Ty::String("r".into()), Ty::Base(BaseTy::I32)]).is_linear() - ); + assert!(!Ty::Tuple(vec![Ty::String("r".into()), Ty::Base(BaseTy::I32)]).is_linear()); } } diff --git a/src/ephapax-typing/src/lib.rs b/src/ephapax-typing/src/lib.rs index ebab4f38..a8176fd3 100644 --- a/src/ephapax-typing/src/lib.rs +++ b/src/ephapax-typing/src/lib.rs @@ -607,11 +607,7 @@ fn sub_patterns_for(p: &Pattern, arity: usize) -> Vec { /// Specialize the matrix on `tag` with the given constructor arity. /// Keeps rows whose head matches the tag and any row whose head is a /// wildcard/var (expanded into `arity` wildcards). -fn specialize_matrix( - matrix: &[Vec], - tag: &Tag, - arity: usize, -) -> Vec> { +fn specialize_matrix(matrix: &[Vec], tag: &Tag, arity: usize) -> Vec> { let mut out = Vec::with_capacity(matrix.len()); for row in matrix { let head = &row[0]; @@ -669,10 +665,7 @@ fn column_type(matrix: &[Vec]) -> ColTy { /// If `col` has a finite signature, return `(Tag, arity)` for each /// constructor in declaration order. Returns `None` for infinite /// domains (integers, floats, opaque types). -fn complete_signature( - col: &ColTy, - registry: &DataCtorRegistry, -) -> Option> { +fn complete_signature(col: &ColTy, registry: &DataCtorRegistry) -> Option> { match col { ColTy::Bool => Some(vec![(Tag::Bool(false), 0), (Tag::Bool(true), 0)]), ColTy::Unit => Some(vec![(Tag::Unit, 0)]), @@ -740,10 +733,8 @@ fn is_useful( .expect("not all_present implies a missing tag"); let default = default_matrix(matrix); if let Some(tail_witness) = is_useful(&default, num_cols - 1, registry) { - let head = reconstruct_witness( - missing_tag, - vec![Witness::Wildcard; *missing_arity], - ); + let head = + reconstruct_witness(missing_tag, vec![Witness::Wildcard; *missing_arity]); let mut out = Vec::with_capacity(num_cols); out.push(head); out.extend(tail_witness); @@ -837,14 +828,26 @@ impl TypeChecker { // Unification variable cases (Ty::Unif(id), _) => { if b.contains_unif(*id) { - return Err(self.at(s, TypeError::TypeMismatch { expected: a, found: b })); + return Err(self.at( + s, + TypeError::TypeMismatch { + expected: a, + found: b, + }, + )); } self.unif_solutions.insert(*id, b); Ok(()) } (_, Ty::Unif(id)) => { if a.contains_unif(*id) { - return Err(self.at(s, TypeError::TypeMismatch { expected: a, found: b })); + return Err(self.at( + s, + TypeError::TypeMismatch { + expected: a, + found: b, + }, + )); } self.unif_solutions.insert(*id, a); Ok(()) @@ -855,30 +858,70 @@ impl TypeChecker { self.unify(s, p1, p2)?; self.unify(s, r1, r2) } - (Ty::Prod { left: l1, right: r1 }, Ty::Prod { left: l2, right: r2 }) => { + ( + Ty::Prod { + left: l1, + right: r1, + }, + Ty::Prod { + left: l2, + right: r2, + }, + ) => { self.unify(s, l1, l2)?; self.unify(s, r1, r2) } - (Ty::Sum { left: l1, right: r1 }, Ty::Sum { left: l2, right: r2 }) => { + ( + Ty::Sum { + left: l1, + right: r1, + }, + Ty::Sum { + left: l2, + right: r2, + }, + ) => { self.unify(s, l1, l2)?; self.unify(s, r1, r2) } - (Ty::Ref { linearity: l1, inner: i1 }, Ty::Ref { linearity: l2, inner: i2 }) - if l1 == l2 => - { - self.unify(s, i1, i2) - } - (Ty::Region { name: n1, inner: i1 }, Ty::Region { name: n2, inner: i2 }) - if n1 == n2 => - { - self.unify(s, i1, i2) - } - (Ty::Borrow { inner: i1, mutable: m1 }, Ty::Borrow { inner: i2, mutable: m2 }) => { + ( + Ty::Ref { + linearity: l1, + inner: i1, + }, + Ty::Ref { + linearity: l2, + inner: i2, + }, + ) if l1 == l2 => self.unify(s, i1, i2), + ( + Ty::Region { + name: n1, + inner: i1, + }, + Ty::Region { + name: n2, + inner: i2, + }, + ) if n1 == n2 => self.unify(s, i1, i2), + ( + Ty::Borrow { + inner: i1, + mutable: m1, + }, + Ty::Borrow { + inner: i2, + mutable: m2, + }, + ) => { if m1 != m2 { - return Err(self.at(s, TypeError::TypeMismatch { - expected: a.clone(), - found: b.clone(), - })); + return Err(self.at( + s, + TypeError::TypeMismatch { + expected: a.clone(), + found: b.clone(), + }, + )); } self.unify(s, i1, i2) } @@ -891,7 +934,13 @@ impl TypeChecker { } // Mismatch - _ => Err(self.at(s, TypeError::TypeMismatch { expected: a, found: b })), + _ => Err(self.at( + s, + TypeError::TypeMismatch { + expected: a, + found: b, + }, + )), } } @@ -1050,8 +1099,7 @@ impl TypeChecker { value_ty }; - self.ctx - .extend(name.clone(), resolved_ty, BindingForm::Let); + self.ctx.extend(name.clone(), resolved_ty, BindingForm::Let); let body_ty = self.check(body)?; // `let` is AFFINE — unconsumed bindings are allowed (implicit drop). @@ -1071,10 +1119,7 @@ impl TypeChecker { if let Some(entry) = self.ctx.vars.get(param) { if entry.demands_consumption() && !entry.used { - return Err(self.at( - s, - TypeError::LinearVariableNotConsumed(param.clone()), - )); + return Err(self.at(s, TypeError::LinearVariableNotConsumed(param.clone()))); } } @@ -1084,12 +1129,7 @@ impl TypeChecker { }) } - fn check_app( - &mut self, - s: Span, - func: &Expr, - arg: &Expr, - ) -> Result { + fn check_app(&mut self, s: Span, func: &Expr, arg: &Expr) -> Result { let func_ty = self.check(func)?; let arg_ty = self.check(arg)?; @@ -1201,7 +1241,12 @@ impl TypeChecker { Ok(body_ty) } - fn check_borrow(&mut self, s: Span, inner: &Expr, mutable: bool) -> Result { + fn check_borrow( + &mut self, + s: Span, + inner: &Expr, + mutable: bool, + ) -> Result { match &inner.kind { ExprKind::Var(name) => { let ty = self @@ -1209,11 +1254,17 @@ impl TypeChecker { .lookup(name) .ok_or_else(|| self.at(s, TypeError::UnboundVariable(name.clone())))? .clone(); - Ok(Ty::Borrow { inner: Box::new(ty), mutable }) + Ok(Ty::Borrow { + inner: Box::new(ty), + mutable, + }) } _ => { let inner_ty = self.check(inner)?; - Ok(Ty::Borrow { inner: Box::new(inner_ty), mutable }) + Ok(Ty::Borrow { + inner: Box::new(inner_ty), + mutable, + }) } } } @@ -1246,7 +1297,9 @@ impl TypeChecker { match inner_ty { Ty::String(_) => Ok(Ty::Base(BaseTy::I32)), - Ty::Borrow { inner: ref boxed, .. } => match boxed.as_ref() { + Ty::Borrow { + inner: ref boxed, .. + } => match boxed.as_ref() { Ty::String(_) => Ok(Ty::Base(BaseTy::I32)), _ => Err(self.at( s, @@ -1295,10 +1348,7 @@ impl TypeChecker { // let! bindings MUST be consumed — regardless of type. if let Some(entry) = self.ctx.vars.get(name) { if !entry.used { - return Err(self.at( - s, - TypeError::LinearVariableNotConsumed(name.clone()), - )); + return Err(self.at(s, TypeError::LinearVariableNotConsumed(name.clone()))); } } @@ -1321,10 +1371,9 @@ impl TypeChecker { match inner_ty { Ty::Prod { left, right } => { if right.is_linear() { - return Err(self.at( - s, - TypeError::LinearVariableNotConsumed("_pair_snd".into()), - )); + return Err( + self.at(s, TypeError::LinearVariableNotConsumed("_pair_snd".into())) + ); } Ok(*left) } @@ -1347,10 +1396,9 @@ impl TypeChecker { match inner_ty { Ty::Prod { left, right } => { if left.is_linear() { - return Err(self.at( - s, - TypeError::LinearVariableNotConsumed("_pair_fst".into()), - )); + return Err( + self.at(s, TypeError::LinearVariableNotConsumed("_pair_fst".into())) + ); } Ok(*right) } @@ -1407,10 +1455,9 @@ impl TypeChecker { if let Some(entry) = self.ctx.vars.get(left_var) { if entry.demands_consumption() && !entry.used { - return Err(self.at( - s, - TypeError::LinearVariableNotConsumed(left_var.clone()), - )); + return Err( + self.at(s, TypeError::LinearVariableNotConsumed(left_var.clone())) + ); } } @@ -1426,10 +1473,9 @@ impl TypeChecker { if let Some(entry) = self.ctx.vars.get(right_var) { if entry.demands_consumption() && !entry.used { - return Err(self.at( - s, - TypeError::LinearVariableNotConsumed(right_var.clone()), - )); + return Err( + self.at(s, TypeError::LinearVariableNotConsumed(right_var.clone())) + ); } } @@ -1499,8 +1545,7 @@ impl TypeChecker { for name in &bound { if let Some(entry) = self.ctx.vars.get(name) { if entry.demands_consumption() && !entry.used { - return Err(self - .at(s, TypeError::LinearVariableNotConsumed(name.clone()))); + return Err(self.at(s, TypeError::LinearVariableNotConsumed(name.clone()))); } } } @@ -1546,8 +1591,7 @@ impl TypeChecker { match pattern { Pattern::Wildcard => Ok(Vec::new()), Pattern::Var(name) => { - self.ctx - .extend(name.clone(), expected, BindingForm::Param); + self.ctx.extend(name.clone(), expected, BindingForm::Param); Ok(vec![name.clone()]) } Pattern::Literal(lit) => { @@ -1697,7 +1741,10 @@ impl TypeChecker { _ => Err(self.at( s, TypeError::TypeMismatch { - expected: Ty::Borrow { inner: Box::new(Ty::Base(BaseTy::Unit)), mutable: false }, + expected: Ty::Borrow { + inner: Box::new(Ty::Base(BaseTy::Unit)), + mutable: false, + }, found: inner_ty, }, )), @@ -2003,8 +2050,7 @@ impl TypeChecker { // Parameters get fresh unification variables for param in &clause.params { let param_ty = self.fresh_unif(); - self.ctx - .extend(param.clone(), param_ty, BindingForm::Let); + self.ctx.extend(param.clone(), param_ty, BindingForm::Let); } // If resume mode is specified, add a resume callback to scope @@ -2016,10 +2062,9 @@ impl TypeChecker { // linear captures is a type error for (name, entry) in &self.ctx.vars { if entry.demands_consumption() && !entry.used { - return Err(self.at( - s, - TypeError::LinearVariableNotConsumed(name.clone()), - )); + return Err( + self.at(s, TypeError::LinearVariableNotConsumed(name.clone())) + ); } } } @@ -2124,15 +2169,16 @@ impl ModuleRegistry { ret: Box::new(acc), }) }; - let poly_ty = - type_params.iter().rev().fold(fn_ty, |acc, tv| Ty::ForAll { - var: tv.clone(), - body: Box::new(acc), - }); + let poly_ty = type_params.iter().rev().fold(fn_ty, |acc, tv| Ty::ForAll { + var: tv.clone(), + body: Box::new(acc), + }); entries.push((name.clone(), poly_ty, *visibility)); } Decl::Type { - name, visibility, ty, + name, + visibility, + ty, } => { entries.push((name.clone(), ty.clone(), *visibility)); } @@ -2163,13 +2209,13 @@ impl ModuleRegistry { ret: Box::new(ret_ty.clone()), } } else { - params.iter().rev().fold( - ret_ty.clone(), - |acc, (_, param_ty)| Ty::Fun { + params + .iter() + .rev() + .fold(ret_ty.clone(), |acc, (_, param_ty)| Ty::Fun { param: Box::new(param_ty.clone()), ret: Box::new(acc), - }, - ) + }) }; entries.push((name.clone(), fn_ty, Visibility::Public)); } @@ -2237,9 +2283,10 @@ pub fn type_check_module_with_registry( } else { // Import resolution error — use dummy span since Import has no span return Err(SpannedTypeError { - error: TypeError::UnboundVariable( - Var::from(format!("{}::{}", import.module, name)), - ), + error: TypeError::UnboundVariable(Var::from(format!( + "{}::{}", + import.module, name + ))), span: Span::dummy(), }); } @@ -2257,10 +2304,7 @@ pub fn type_check_module_with_registry( } /// Internal module checking logic shared by single-module and registry paths. -fn type_check_module_inner( - tc: &mut TypeChecker, - module: &Module, -) -> Result<(), SpannedTypeError> { +fn type_check_module_inner(tc: &mut TypeChecker, module: &Module) -> Result<(), SpannedTypeError> { // Pre-pass: register all `Decl::Data` declarations so constructor // patterns inside function bodies can resolve via the registry. tc.data_registry @@ -2327,13 +2371,13 @@ fn type_check_module_inner( ret: Box::new(ret_ty.clone()), } } else { - params.iter().rev().fold( - ret_ty.clone(), - |acc, (_, param_ty)| Ty::Fun { + params + .iter() + .rev() + .fold(ret_ty.clone(), |acc, (_, param_ty)| Ty::Fun { param: Box::new(param_ty.clone()), ret: Box::new(acc), - }, - ) + }) }; tc.ctx.extend(name.clone(), fn_ty, BindingForm::Let); } @@ -3061,7 +3105,10 @@ mod tests { fn test_span_propagation() { // Verify that error spans point to the right expression. let mut tc = TypeChecker::new(); - let inner = Expr::new(ExprKind::Lit(Literal::String("bare".into())), Span::new(10, 25)); + let inner = Expr::new( + ExprKind::Lit(Literal::String("bare".into())), + Span::new(10, 25), + ); let outer = Expr::new( ExprKind::Let { name: "x".into(), @@ -3292,10 +3339,14 @@ mod tests { let s = Span::dummy(); // Unify I32 with I32 — should succeed - assert!(tc.unify(s, &Ty::Base(BaseTy::I32), &Ty::Base(BaseTy::I32)).is_ok()); + assert!(tc + .unify(s, &Ty::Base(BaseTy::I32), &Ty::Base(BaseTy::I32)) + .is_ok()); // Unify I32 with Bool — should fail - assert!(tc.unify(s, &Ty::Base(BaseTy::I32), &Ty::Base(BaseTy::Bool)).is_err()); + assert!(tc + .unify(s, &Ty::Base(BaseTy::I32), &Ty::Base(BaseTy::Bool)) + .is_err()); // Unify ?0 with I32 — should succeed and solve ?0 = I32 let u = tc.fresh_unif(); @@ -3707,7 +3758,10 @@ mod tests { assert_eq!(some.fields, vec![Ty::Var("a".into())]); let info = tc.data_registry.get_type("Option").unwrap(); - assert_eq!(info.ctor_names, vec![SmolStr::from("None"), SmolStr::from("Some")]); + assert_eq!( + info.ctor_names, + vec![SmolStr::from("None"), SmolStr::from("Some")] + ); } #[test] @@ -3929,7 +3983,11 @@ mod tests { assert!( matches!( err.error, - TypeError::ConstructorArityMismatch { expected: 1, got: 2, .. } + TypeError::ConstructorArityMismatch { + expected: 1, + got: 2, + .. + } ), "got {:?}", err.error @@ -4026,7 +4084,11 @@ mod tests { decls: vec![fn_decl("f", vec![], Ty::Base(BaseTy::I32), body)], }; let err = type_check_module(&module).unwrap_err(); - assert!(matches!(err.error, TypeError::EmptyMatch), "got {:?}", err.error); + assert!( + matches!(err.error, TypeError::EmptyMatch), + "got {:?}", + err.error + ); } /// Multi-param data type with substitution: `Result(I32, Bool)`. @@ -4186,8 +4248,7 @@ mod tests { fn_decl("f", vec![], Ty::Base(BaseTy::I32), body), ], }; - type_check_module(&module) - .expect("nested Option(Option(_)) should be exhaustive"); + type_check_module(&module).expect("nested Option(Option(_)) should be exhaustive"); } /// `Option(Option(I32))` with `None` + `Some(Some(_))` only — @@ -4320,9 +4381,8 @@ mod tests { fn_decl("f", vec![], Ty::Base(BaseTy::I32), body), ], }; - type_check_module(&module).expect( - "cross product (Option, Bool) of all combinations should be exhaustive", - ); + type_check_module(&module) + .expect("cross product (Option, Bool) of all combinations should be exhaustive"); } /// Tuple of `(Option(I32), Bool)` missing `(Some(_), false)` — diff --git a/src/ephapax-wasm/src/carriers.rs b/src/ephapax-wasm/src/carriers.rs index c15870b2..240d60f4 100644 --- a/src/ephapax-wasm/src/carriers.rs +++ b/src/ephapax-wasm/src/carriers.rs @@ -177,9 +177,24 @@ mod tests { #[test] fn access_sites_round_trip() { let entries = vec![ - AccessSite { func_idx: 3, byte_offset: 12, region_id: 0, field_id: 0 }, - AccessSite { func_idx: 3, byte_offset: 19, region_id: 0, field_id: 1 }, - AccessSite { func_idx: 300, byte_offset: 70_000, region_id: 0, field_id: 1 }, + AccessSite { + func_idx: 3, + byte_offset: 12, + region_id: 0, + field_id: 0, + }, + AccessSite { + func_idx: 3, + byte_offset: 19, + region_id: 0, + field_id: 1, + }, + AccessSite { + func_idx: 300, + byte_offset: 70_000, + region_id: 0, + field_id: 1, + }, ]; let payload = build_access_sites_payload(&entries); assert_eq!(parse_access_sites_payload(&payload), Some(entries)); @@ -220,7 +235,7 @@ mod tests { assert_eq!(&p[6..10], &14u32.to_le_bytes()); // name len assert_eq!(&p[10..24], b"ephapax.string"); assert_eq!(&p[24..28], &2u32.to_le_bytes()); // two fields - // trailing region_byte_size == 8 + // trailing region_byte_size == 8 assert_eq!(&p[p.len() - 4..], &8u32.to_le_bytes()); } } diff --git a/src/ephapax-wasm/src/lib.rs b/src/ephapax-wasm/src/lib.rs index 0f7422c6..c9a0de87 100644 --- a/src/ephapax-wasm/src/lib.rs +++ b/src/ephapax-wasm/src/lib.rs @@ -46,8 +46,8 @@ //! //! Mode is set per-module compilation and affects how unused linear values are handled. -mod debug; pub mod carriers; +mod debug; pub mod ownership; pub use debug::{DebugInfo, VariableMetadata}; @@ -186,7 +186,10 @@ fn ty_to_ownership_kind(ty: &Ty) -> ownership::OwnershipKind { match ty { Ty::Borrow { mutable: true, .. } => ownership::OwnershipKind::ExclBorrow, Ty::Borrow { mutable: false, .. } => ownership::OwnershipKind::SharedBorrow, - Ty::Ref { linearity: Linearity::Linear, .. } => ownership::OwnershipKind::Linear, + Ty::Ref { + linearity: Linearity::Linear, + .. + } => ownership::OwnershipKind::Linear, Ty::String(_) => ownership::OwnershipKind::Linear, Ty::Region { inner, .. } => ty_to_ownership_kind(inner), Ty::ForAll { body, .. } => ty_to_ownership_kind(body), @@ -778,8 +781,10 @@ impl Codegen { let param_names: Vec = params.iter().map(|(n, _)| n.to_string()).collect(); - let param_kinds: Vec = - params.iter().map(|(_, ty)| ty_to_ownership_kind(ty)).collect(); + let param_kinds: Vec = params + .iter() + .map(|(_, ty)| ty_to_ownership_kind(ty)) + .collect(); self.user_fns.insert( name.to_string(), @@ -941,16 +946,14 @@ impl Codegen { // position among ALL imports: builtin host // imports occupy 0..NUM_BUILTIN_IMPORTS, // then this extern fn takes the next slot. - let import_idx = - NUM_BUILTIN_IMPORTS + self.extern_imports.len() as u32; + let import_idx = NUM_BUILTIN_IMPORTS + self.extern_imports.len() as u32; self.extern_imports.push(ExternImport { abi: abi.clone(), name: name.to_string(), wasm_type_idx: type_idx, }); - self.extern_fn_indices - .insert(name.to_string(), import_idx); + self.extern_fn_indices.insert(name.to_string(), import_idx); } } } @@ -1095,7 +1098,11 @@ impl Codegen { for decl in &ast.decls { match decl { Decl::Fn { - name, params, body, type_params: _, .. + name, + params, + body, + type_params: _, + .. } => { let info = self.user_fns.get(name.as_str()).cloned().ok_or_else(|| { CodegenError(format!("BUG: function `{}` not collected", name)) @@ -1264,7 +1271,12 @@ impl Codegen { // Export all user functions by name for decl in &ast.decls { - if let Decl::Fn { name, type_params: _, .. } = decl { + if let Decl::Fn { + name, + type_params: _, + .. + } = decl + { if let Some(info) = self.user_fns.get(name.as_str()) { exports.export(name.as_str(), ExportKind::Func, info.wasm_fn_idx); } @@ -1275,17 +1287,41 @@ impl Codegen { } fn add_runtime_exports(&self, exports: &mut ExportSection) { - exports.export("__ephapax_bump_alloc", ExportKind::Func, self.fn_bump_alloc()); - exports.export("__ephapax_string_new", ExportKind::Func, self.fn_string_new()); - exports.export("__ephapax_string_len", ExportKind::Func, self.fn_string_len()); + exports.export( + "__ephapax_bump_alloc", + ExportKind::Func, + self.fn_bump_alloc(), + ); + exports.export( + "__ephapax_string_new", + ExportKind::Func, + self.fn_string_new(), + ); + exports.export( + "__ephapax_string_len", + ExportKind::Func, + self.fn_string_len(), + ); exports.export( "__ephapax_string_concat", ExportKind::Func, self.fn_string_concat(), ); - exports.export("__ephapax_string_drop", ExportKind::Func, self.fn_string_drop()); - exports.export("__ephapax_region_enter", ExportKind::Func, self.fn_region_enter()); - exports.export("__ephapax_region_exit", ExportKind::Func, self.fn_region_exit()); + exports.export( + "__ephapax_string_drop", + ExportKind::Func, + self.fn_string_drop(), + ); + exports.export( + "__ephapax_region_enter", + ExportKind::Func, + self.fn_region_enter(), + ); + exports.export( + "__ephapax_region_exit", + ExportKind::Func, + self.fn_region_exit(), + ); exports.export("memory", ExportKind::Memory, 0); } @@ -2144,9 +2180,8 @@ impl Codegen { // 3. Calculate function index for this lambda. Lambdas live // after the first user fn block, shifted forward by any // dynamic imports (extern / FFI) registered earlier. - let lambda_fn_idx = self.first_user_fn() - + self.user_fns.len() as u32 - + self.lambda_fns.len() as u32; + let lambda_fn_idx = + self.first_user_fn() + self.user_fns.len() as u32 + self.lambda_fns.len() as u32; // 4. Table index: position in the function table (0-based) let table_idx = self.user_fns.len() as u32 + self.lambda_fns.len() as u32; @@ -3063,10 +3098,7 @@ fn ty_to_valtype(ty: &Ty) -> ValType { /// Used by `compile_app` (and `collect_extern_imports`-aware callers) /// to recognise direct calls to named functions even when the surface /// parser has curried the call into nested `App` nodes. -fn flatten_app_chain<'a>( - fn_expr: &'a Expr, - arg: &'a Expr, -) -> Option<(&'a str, Vec<&'a Expr>)> { +fn flatten_app_chain<'a>(fn_expr: &'a Expr, arg: &'a Expr) -> Option<(&'a str, Vec<&'a Expr>)> { let mut args: Vec<&Expr> = vec![arg]; let mut current = fn_expr; loop { @@ -4718,8 +4750,7 @@ mod tests { fn ownership_payload(wasm: &[u8]) -> Option> { use wasmparser::{Parser as WP, Payload}; for payload in WP::new(0).parse_all(wasm) { - if let Payload::CustomSection(reader) = payload.expect("wasm parse") - { + if let Payload::CustomSection(reader) = payload.expect("wasm parse") { if reader.name() == ownership::OWNERSHIP_SECTION_NAME { return Some(reader.data().to_vec()); } @@ -4770,8 +4801,8 @@ mod tests { let payload = custom_payload(&wasm, carriers::ACCESS_SITES_SECTION_NAME) .expect("typedwasm.access-sites section present"); - let entries = carriers::parse_access_sites_payload(&payload) - .expect("access-sites payload parses"); + let entries = + carriers::parse_access_sites_payload(&payload).expect("access-sites payload parses"); // string_new (2 stores) + string_len (1 load) + string_concat // (4 loads + 2 stores) = 9 typed accesses. assert_eq!(entries.len(), 9); @@ -4794,8 +4825,7 @@ mod tests { } for entry in &entries { assert_eq!(entry.region_id, carriers::REGION_STRING); - assert!(entry.field_id == carriers::FIELD_PTR - || entry.field_id == carriers::FIELD_LEN); + assert!(entry.field_id == carriers::FIELD_PTR || entry.field_id == carriers::FIELD_LEN); let body_idx = (entry.func_idx - import_count) as usize; let (start, end) = bodies[body_idx]; let opcode_at = start + entry.byte_offset as usize; @@ -4804,7 +4834,9 @@ mod tests { assert!( opcode == 0x28 || opcode == 0x36, "offset {} in func {} points at 0x{:02x}, not i32.load/i32.store", - entry.byte_offset, entry.func_idx, opcode + entry.byte_offset, + entry.func_idx, + opcode ); } } @@ -4836,10 +4868,7 @@ mod tests { entries[0].param_kinds, vec![ownership::OwnershipKind::Linear] ); - assert_eq!( - entries[0].ret_kind, - ownership::OwnershipKind::Unrestricted - ); + assert_eq!(entries[0].ret_kind, ownership::OwnershipKind::Unrestricted); } #[test] @@ -5142,8 +5171,7 @@ mod tests { }, ], }; - let wasm = - compile_module(&module).expect("Some(0) | Some(v) | None should compile"); + let wasm = compile_module(&module).expect("Some(0) | Some(v) | None should compile"); assert_wasm_header(&wasm); validate_wasm(&wasm); } @@ -5213,8 +5241,7 @@ mod tests { }, ], }; - let wasm = - compile_module(&module).expect("Box(Some(v)) | Box(None) should compile"); + let wasm = compile_module(&module).expect("Box(Some(v)) | Box(None) should compile"); assert_wasm_header(&wasm); validate_wasm(&wasm); } @@ -5280,8 +5307,7 @@ mod tests { }, ], }; - let wasm = - compile_module(&module).expect("guarded match should compile"); + let wasm = compile_module(&module).expect("guarded match should compile"); assert_wasm_header(&wasm); validate_wasm(&wasm); } @@ -5333,8 +5359,7 @@ mod tests { }, ], }; - let wasm = - compile_module(&module).expect("guard on wildcard should compile"); + let wasm = compile_module(&module).expect("guard on wildcard should compile"); assert_wasm_header(&wasm); validate_wasm(&wasm); } diff --git a/tests/fuzz/src/lib.rs b/tests/fuzz/src/lib.rs index 9c0d3f42..9f3c3729 100644 --- a/tests/fuzz/src/lib.rs +++ b/tests/fuzz/src/lib.rs @@ -3,8 +3,8 @@ // SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell //! Fuzz testing infrastructure for Ephapax -//! +//! //! This crate provides fuzz targets for cargo-fuzz to test: //! - Parser robustness //! - Type checker correctness -//! - WASM code generation safety \ No newline at end of file +//! - WASM code generation safety