diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index a551f46..4d805c0 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -14,16 +14,16 @@ workflows: - 'actions/checkout@v7.0.1' - 'mlugg/setup-zig@v2.2.1' '.github/workflows/governance.yml': - - 'hyperpolymath/standards@571cc734cd69fb846032ec77a662aa8ee4fc32cd' + - 'metadatastician/standards@be39f900665fb7c042c05d1165da018adc1860f4' '.github/workflows/hypatia-scan.yml': - - 'hyperpolymath/standards@571cc734cd69fb846032ec77a662aa8ee4fc32cd' + - 'metadatastician/standards@be39f900665fb7c042c05d1165da018adc1860f4' '.github/workflows/instant-sync.yml': - 'peter-evans/repository-dispatch@v4.0.1' '.github/workflows/label-triage.yml': [] '.github/workflows/labels.yml': [] '.github/workflows/lock-sync-gate.yml': [] '.github/workflows/mirror.yml': - - 'hyperpolymath/standards@571cc734cd69fb846032ec77a662aa8ee4fc32cd' + - 'metadatastician/standards@be39f900665fb7c042c05d1165da018adc1860f4' '.github/workflows/push-email-notify.yml': - 'dawidd6/action-send-mail@27bf7a883db6de205849f760badd38a1a3ce0dd0' '.github/workflows/rust-ci.yml': @@ -31,9 +31,9 @@ workflows: - 'dtolnay/rust-toolchain@v1' - 'swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0' '.github/workflows/scorecard.yml': - - 'hyperpolymath/standards@571cc734cd69fb846032ec77a662aa8ee4fc32cd' + - 'metadatastician/standards@be39f900665fb7c042c05d1165da018adc1860f4' '.github/workflows/secret-scanner.yml': - - 'hyperpolymath/standards@571cc734cd69fb846032ec77a662aa8ee4fc32cd' + - 'metadatastician/standards@be39f900665fb7c042c05d1165da018adc1860f4' '.github/workflows/status-gate.yml': - 'actions/checkout@v7.0.1' - 'dtolnay/rust-toolchain@v1' @@ -54,9 +54,14 @@ dependencies: commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1' owner_id: 44036562 repo_id: 197814629 - 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a': - ref: 'v7.0.1' - commit: 'sha1-043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' + 'actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333': + ref: 'v8.0.2' + commit: 'sha1-9000827ccba6bdab643e8b6fd33ac0654aef8333' + owner_id: 44036562 + repo_id: 192626254 + 'actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9': + ref: 'v7.0.2' + commit: 'sha1-cf430e030ddbb5b0abf93d22962f4752f3646cd9' owner_id: 44036562 repo_id: 192625955 'dawidd6/action-send-mail@27bf7a883db6de205849f760badd38a1a3ce0dd0': @@ -69,9 +74,9 @@ dependencies: commit: 'sha1-22d081ff2d3a40755e97629de92e3bcbfa7cf2ed' owner_id: 42048915 repo_id: 356423100 - 'dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772': - ref: '6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772' - commit: 'sha1-6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772' + 'dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8': + ref: 'v1' + commit: 'sha1-683b588c487a69d01f6c6ddf40f036ff682cc2c8' owner_id: 1940490 repo_id: 260749683 'dtolnay/rust-toolchain@v1': @@ -79,9 +84,9 @@ dependencies: commit: 'sha1-02cb101ec7c40f2c49e1d9714d64511d8e1b74de' owner_id: 1940490 repo_id: 260749683 - 'editorconfig-checker/action-editorconfig-checker@840e866d93b8e032123c23bac69dece044d4d84c': - ref: 'v2.2.0' - commit: 'sha1-840e866d93b8e032123c23bac69dece044d4d84c' + 'editorconfig-checker/action-editorconfig-checker@51f63319f592f97930c73d9c46184d20bd206393': + ref: 'v3.0.0' + commit: 'sha1-51f63319f592f97930c73d9c46184d20bd206393' owner_id: 26415196 repo_id: 297874902 'erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124': @@ -94,9 +99,9 @@ dependencies: commit: 'sha1-33d7b85da1a04171e211befe2c739303b9a4c066' owner_id: 9919 repo_id: 259445878 - 'github/codeql-action@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28': - ref: 'v4.37.8' - commit: 'sha1-db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28' + 'github/codeql-action@b96794f015dfd88f77b49b1c93e0fa7110f94c63': + ref: 'v4.38.0' + commit: 'sha1-b96794f015dfd88f77b49b1c93e0fa7110f94c63' owner_id: 9919 repo_id: 259445878 'goto-bus-stop/setup-zig@abea47f85e598557f500fa1fd2ab7464fcb39406': @@ -104,44 +109,46 @@ dependencies: commit: 'sha1-abea47f85e598557f500fa1fd2ab7464fcb39406' owner_id: 1006268 repo_id: 212984112 - 'hyperpolymath/standards@571cc734cd69fb846032ec77a662aa8ee4fc32cd': - ref: '571cc734cd69fb846032ec77a662aa8ee4fc32cd' - commit: 'sha1-571cc734cd69fb846032ec77a662aa8ee4fc32cd' - owner_id: 6759885 + 'metadatastician/standards@be39f900665fb7c042c05d1165da018adc1860f4': + ref: 'be39f900665fb7c042c05d1165da018adc1860f4' + commit: 'sha1-be39f900665fb7c042c05d1165da018adc1860f4' + owner_id: 297579217 repo_id: 1116521501 uses: - 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9' - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' - - 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' + - 'actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333' + - 'actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9' - 'denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed' - - 'dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772' - - 'editorconfig-checker/action-editorconfig-checker@840e866d93b8e032123c23bac69dece044d4d84c' + - 'dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8' + - 'editorconfig-checker/action-editorconfig-checker@51f63319f592f97930c73d9c46184d20bd206393' - 'erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124' - - 'github/codeql-action@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28' + - 'github/codeql-action@b96794f015dfd88f77b49b1c93e0fa7110f94c63' - 'goto-bus-stop/setup-zig@abea47f85e598557f500fa1fd2ab7464fcb39406' - - 'ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc' - - 'swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6' + - 'ossf/scorecard-action@fa7117db83c52d787ae5a89f350999701505d78e' + - 'oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6' + - 'swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0' - 'webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555' 'mlugg/setup-zig@v2.2.1': ref: 'v2.2.1' commit: 'sha1-d1434d08867e3ee9daa34448df10607b98908d29' owner_id: 7289241 repo_id: 812112570 - 'ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc': - ref: 'v2.4.4' - commit: 'sha1-2d1146689b8cda280b9bc96326124645441f03bc' + 'ossf/scorecard-action@fa7117db83c52d787ae5a89f350999701505d78e': + ref: 'v2.5.0' + commit: 'sha1-fa7117db83c52d787ae5a89f350999701505d78e' owner_id: 67707773 repo_id: 421101922 + 'oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6': + ref: 'v2.2.0' + commit: 'sha1-0c5077e51419868618aeaa5fe8019c62421857d6' + owner_id: 108928776 + repo_id: 512644635 'peter-evans/repository-dispatch@v4.0.1': ref: 'v4.0.1' commit: 'sha1-28959ce8df70de7be546dd1250a005dd32156697' owner_id: 18365890 repo_id: 220359305 - 'swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6': - ref: 'v2.9.2' - commit: 'sha1-6323deb102c322ba6fcbdcafc7e3dddab59af2b6' - owner_id: 580492 - repo_id: 298565987 'swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0': ref: 'v2.9.3' commit: 'sha1-d1527c8f67ef9af357290475d4af4b0fab8f12e0' diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index 79ba1dd..36ed853 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -16,4 +16,4 @@ permissions: jobs: governance: - uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@571cc734cd69fb846032ec77a662aa8ee4fc32cd + uses: metadatastician/standards/.github/workflows/governance-reusable.yml@be39f900665fb7c042c05d1165da018adc1860f4 diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml index af22c72..4edfda3 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -19,4 +19,4 @@ permissions: jobs: hypatia: - uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@571cc734cd69fb846032ec77a662aa8ee4fc32cd + uses: metadatastician/standards/.github/workflows/hypatia-scan-reusable.yml@be39f900665fb7c042c05d1165da018adc1860f4 diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml index 5440e46..7080a27 100644 --- a/.github/workflows/mirror.yml +++ b/.github/workflows/mirror.yml @@ -14,5 +14,16 @@ permissions: jobs: mirror: - uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@571cc734cd69fb846032ec77a662aa8ee4fc32cd - secrets: inherit + uses: metadatastician/standards/.github/workflows/mirror-reusable.yml@be39f900665fb7c042c05d1165da018adc1860f4 + # Named secrets only; `secrets: inherit` forwards every secret (standards#1108 §6b). + secrets: + GITLAB_SSH_KEY: ${{ secrets.GITLAB_SSH_KEY }} + BITBUCKET_SSH_KEY: ${{ secrets.BITBUCKET_SSH_KEY }} + CODEBERG_SSH_KEY: ${{ secrets.CODEBERG_SSH_KEY }} + SOURCEHUT_SSH_KEY: ${{ secrets.SOURCEHUT_SSH_KEY }} + DISROOT_SSH_KEY: ${{ secrets.DISROOT_SSH_KEY }} + GITEA_SSH_KEY: ${{ secrets.GITEA_SSH_KEY }} + RADICLE_KEY: ${{ secrets.RADICLE_KEY }} + R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} + R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} + R2_AGE_RECIPIENT: ${{ secrets.R2_AGE_RECIPIENT }} diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index c6ff464..0e942a1 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -32,7 +32,7 @@ concurrency: jobs: rust-ci: - uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@571cc734cd69fb846032ec77a662aa8ee4fc32cd + uses: metadatastician/standards/.github/workflows/rust-ci-reusable.yml@be39f900665fb7c042c05d1165da018adc1860f4 no-default-features: name: Cargo build + test (ephapax-cli, --no-default-features) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index f2ec1f8..08678f1 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -15,7 +15,8 @@ permissions: jobs: scorecard: permissions: + actions: read contents: read security-events: write id-token: write - uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@571cc734cd69fb846032ec77a662aa8ee4fc32cd + uses: metadatastician/standards/.github/workflows/scorecard-reusable.yml@be39f900665fb7c042c05d1165da018adc1860f4 diff --git a/.github/workflows/secret-scanner.yml b/.github/workflows/secret-scanner.yml index 80b7c85..a7027d7 100644 --- a/.github/workflows/secret-scanner.yml +++ b/.github/workflows/secret-scanner.yml @@ -22,5 +22,4 @@ jobs: contents: read pull-requests: write actions: read - uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@571cc734cd69fb846032ec77a662aa8ee4fc32cd - secrets: inherit + uses: metadatastician/standards/.github/workflows/secret-scanner-reusable.yml@be39f900665fb7c042c05d1165da018adc1860f4 diff --git a/TEST-NEEDS.adoc b/TEST-NEEDS.adoc index b01faaf..f7a7f04 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: *554 tests* (`+cargo test --workspace --all-targets+`); +* Total: *568 tests* (`+cargo test --workspace --all-targets+`); pass/fail enforced by `+rust-ci.yml+` -* Documented all-target tests: 554 +* Documented all-target tests: 568 === CRG Testing Taxonomy — Status diff --git a/scripts/probe-battery.sh b/scripts/probe-battery.sh index 392496d..7b33620 100755 --- a/scripts/probe-battery.sh +++ b/scripts/probe-battery.sh @@ -81,6 +81,19 @@ done die() { echo "probe-battery: $*" >&2; exit 3; } [ -f "$EXPECT" ] || die "missing $EXPECT" +# GNU timeout reports expiry as 124. BusyBox reports it as 143, the same +# status as a genuine SIGTERM, so a timeout would read as `crash:143`. +timeout --version 2>/dev/null | grep -q GNU || die "GNU coreutils timeout is required" + +# timed CMD...: run under PROBE_TIMEOUT. A command that ignores SIGTERM is +# killed 5 s later; that escalated kill (137 after the deadline) is still a +# timeout, so report it as 124. +timed() { + local start=$SECONDS rc + timeout -k 5 "$PROBE_TIMEOUT" "$@"; rc=$? + if [[ "$rc" -eq 137 && $((SECONDS - start)) -ge "$PROBE_TIMEOUT" ]]; then rc=124; fi + return "$rc" +} [ -f "$ENV_WAT" ] || die "missing $ENV_WAT" if [ -z "$bin" ]; then @@ -171,11 +184,11 @@ run_row() { # PROBE COMMAND -> sets CODE TEXT VERDICT mkdir -p "$stage/out" case "$cmd" in check|run) - TEXT="$(cd "$dir" && timeout "$PROBE_TIMEOUT" "$bin" "$cmd" "$base" 2>&1)" + TEXT="$(cd "$dir" && timed "$bin" "$cmd" "$base" 2>&1)" CODE=$? ;; compile) - TEXT="$(cd "$dir" && timeout "$PROBE_TIMEOUT" "$bin" compile "$base" -o "$wasm" 2>&1)" + TEXT="$(cd "$dir" && timed "$bin" compile "$base" -o "$wasm" 2>&1)" CODE=$? ;; wasm-run) @@ -183,7 +196,7 @@ run_row() { # PROBE COMMAND -> sets CODE TEXT VERDICT # error means there is no module to run (`no-module`); a compile # that times out or dies by a signal is reported as such (`timeout`, # `crash:`), so it cannot pass as SAME against a no-module row. - TEXT="$(cd "$dir" && timeout "$PROBE_TIMEOUT" "$bin" compile "$base" -o "$wasm" 2>&1)" + TEXT="$(cd "$dir" && timed "$bin" compile "$base" -o "$wasm" 2>&1)" CODE=$? if [[ "$CODE" -ne 0 ]]; then if [[ "$CODE" -eq 126 || "$CODE" -eq 127 ]]; then @@ -200,7 +213,7 @@ run_row() { # PROBE COMMAND -> sets CODE TEXT VERDICT if [[ -z "$wasmtime_bin" ]]; then CODE=0; TEXT="wasmtime CLI not found"; VERDICT="unavailable:no-wasmtime"; return fi - TEXT="$(timeout "$PROBE_TIMEOUT" "$wasmtime_bin" run --preload "env=$ENV_WAT" --invoke main "$wasm" 2>&1)" + TEXT="$(timed "$wasmtime_bin" run --preload "env=$ENV_WAT" --invoke main "$wasm" 2>&1)" CODE=$? ;; *) die "unknown command '$cmd' in $EXPECT" ;; diff --git a/src/ephapax-cli/tests/fail_loud.rs b/src/ephapax-cli/tests/fail_loud.rs index 65e6097..a52aef2 100644 --- a/src/ephapax-cli/tests/fail_loud.rs +++ b/src/ephapax-cli/tests/fail_loud.rs @@ -228,6 +228,39 @@ fn codegen_refuses_raw_ffi_in_unchecked_module() { ); } +/// The scan must reach raw FFI below a supported node (here a `let` +/// value), not only at the top of a function body. +#[test] +fn codegen_refuses_raw_ffi_nested_in_a_let_value() { + let ffi = Expr::dummy(ExprKind::FFI { + symbol: "foo".into(), + args: vec![], + }); + let module = Module { + name: "nested".into(), + imports: vec![], + decls: vec![Decl::Fn { + name: "t".into(), + visibility: Visibility::Private, + type_params: vec![], + params: vec![], + ret_ty: Ty::Base(ephapax_syntax::BaseTy::I32), + body: Expr::dummy(ExprKind::Let { + name: "x".into(), + ty: None, + value: Box::new(ffi), + body: Box::new(Expr::dummy(ExprKind::Lit(Literal::I32(0)))), + }), + }], + }; + let err = compile_module(&module).expect_err("nested raw FFI must not lower"); + assert!( + err.0.starts_with(UNSUPPORTED_PREFIX) && err.0.contains("__ffi"), + "{}", + err.0 + ); +} + #[test] fn g5_extern_rewrite_has_contiguous_imports_and_validates() { let dir = tempfile::tempdir().expect("tempdir"); diff --git a/src/ephapax-cli/tests/linearity_holes.rs b/src/ephapax-cli/tests/linearity_holes.rs index ac35b76..d5203ad 100644 --- a/src/ephapax-cli/tests/linearity_holes.rs +++ b/src/ephapax-cli/tests/linearity_holes.rs @@ -104,6 +104,28 @@ fn main(): I32 = } "; +/// Projection from a three-element tuple goes through `check_tuple_index` +/// (a two-element tuple is a pair): `.2` would lose the linear `s`. +const TUPLEIDX3: &str = "\ +fn main(): I32 = + region r { + let s = String.new@r(\"a\") in + let t = (s, 1, 2) in + t.2 + } +"; + +/// Pair destructuring whose body is an `I64` extern call: the lowered +/// core `match` must carry an `i64` result, not the default `i32`. +const DESTRUCT_I64: &str = "\ +extern \"c\" { + fn clock_gettime(): I64 +} +fn f(p: (I32, I32)): I64 = + let (a, b) = p in clock_gettime() +fn main(): I32 = 0 +"; + /// List index from a list of linear strings: the other elements would /// be lost. const LISTIDX: &str = "\ @@ -379,6 +401,15 @@ fn tuple_projection_from_linear_tuple_rejected() { assert_cli_rejects("tupleidx", TUPLEIDX); } +#[test] +fn three_element_tuple_projection_from_linear_tuple_rejected() { + assert!(matches!( + type_error(TUPLEIDX3), + TypeError::LinearProjectionDiscard(Ty::Tuple(_)) + )); + assert_cli_rejects("tupleidx3", TUPLEIDX3); +} + #[test] fn list_index_from_linear_list_rejected() { assert!(matches!( @@ -466,12 +497,31 @@ fn pair_destructuring_lowering_computes_correct_value() { } #[test] -fn multi_field_constructor_destructuring_checks() { - // Type-level only. The wasm for a nested pair payload is wrong on - // the base commit too (`fst((1, (2, 3)))` evaluates to 521 with the - // pre-E0 compiler), a codegen defect outside E0, so this gate does - // not assert the runtime value of a three-field payload. +fn multi_field_constructor_destructuring_computes_correct_value() { + // A three-field payload is a nested pair. Before the bump pointer was + // initialised, the first heap cell overwrote it and this read back + // corrupted fields (`fst((1, (2, 3)))` gave 521). pipeline(DESTRUCT3).expect("three-field constructor destructuring type-checks"); + assert_eq!(compile_and_run("destruct3", DESTRUCT3), 321); +} + +#[test] +fn pair_destructuring_keeps_an_i64_result_type() { + pipeline(DESTRUCT_I64).expect("type-checks"); + let dir = tempfile::tempdir().expect("tempdir"); + let path = write_probe(dir.path(), "destruct_i64", DESTRUCT_I64); + let out = Command::new(ephapax_bin()) + .arg("compile") + .arg(&path) + .arg("-o") + .arg(dir.path().join("destruct_i64.wasm")) + .output() + .expect("run ephapax compile"); + assert!( + out.status.success(), + "an I64 arm body must give valid wasm:\n{}", + String::from_utf8_lossy(&out.stderr) + ); } // --------------------------------------------------------------------- diff --git a/src/ephapax-interp/src/lib.rs b/src/ephapax-interp/src/lib.rs index a90be8e..9a6547a 100644 --- a/src/ephapax-interp/src/lib.rs +++ b/src/ephapax-interp/src/lib.rs @@ -380,23 +380,6 @@ impl Interpreter { Ok(()) } - /// Look up an FFI symbol across all loaded libraries. - fn find_ffi_symbol<'a>( - &'a self, - name: &str, - ) -> Option i64>> - { - for lib in &self.ffi_libraries { - // SAFETY: `lib` is a live, owned library handle borrowed from - // `self.ffi_libraries` for `'a`; the returned Symbol is tied to that - // borrow, so it cannot outlive the library it was resolved from. - if let Ok(sym) = unsafe { lib.get(name.as_bytes()) } { - return Some(sym); - } - } - None - } - /// Load a module (register all function definitions and data decls). pub fn load_module(&mut self, module: &Module) { for decl in &module.decls { @@ -1196,49 +1179,6 @@ impl Interpreter { } /// Call the main function (no arguments) - /// Temporary C string storage for FFI calls. - /// Kept alive for the duration of the FFI call to prevent dangling pointers. - /// Cleared after each FFI call returns. - fn value_to_i64_with_cstrings( - &self, - val: &Value, - cstrings: &mut Vec, - ) -> i64 { - match val { - Value::I32(n) => *n as i64, - Value::I64(n) => *n, - Value::Bool(b) => { - if *b { - 1 - } else { - 0 - } - } - Value::F32(f) => *f as i64, - Value::F64(f) => *f as i64, - Value::Unit => 0, - Value::String { data, .. } => { - // Convert to CString and return pointer as i64. - // The CString is stored in `cstrings` to keep it alive - // until the FFI call completes. - match std::ffi::CString::new(data.as_str()) { - Ok(cstr) => { - // SAFETY: the raw pointer stays valid because `cstr` is - // immediately moved into `cstrings` (kept alive until the - // FFI call completes) on the next line, so `ptr` never - // dangles for the duration of its use. - let ptr = cstr.as_ptr() as i64; - cstrings.push(cstr); - ptr - } - Err(_) => 0, // String contains null byte — return null - } - } - // Compound values: not directly passable to C ABI - _ => 0, - } - } - pub fn call_main(&mut self) -> Result { let name: SmolStr = "main".into(); diff --git a/src/ephapax-typing/src/lib.rs b/src/ephapax-typing/src/lib.rs index 2a9ed85..50452cf 100644 --- a/src/ephapax-typing/src/lib.rs +++ b/src/ephapax-typing/src/lib.rs @@ -4845,6 +4845,28 @@ mod review_regressions { fn expr(kind: ExprKind) -> Expr { Expr::new(kind, Span::dummy()) } + /// Region-exit accounting on its own: the conformance fixture is caught + /// earlier, at `let!` scope exit, so it cannot detect a regression here. + #[test] + fn region_exit_rejects_a_live_unconsumed_linear_binding() { + let mut tc = TypeChecker::new(); + tc.ctx.enter_region("r".into()); + tc.ctx + .extend("conn".into(), Ty::String("r".into()), BindingForm::LetBang); + tc.ctx.exit_region(); + let err = tc + .check_region( + Span::dummy(), + &"r".into(), + &expr(ExprKind::Lit(Literal::I32(42))), + ) + .unwrap_err(); + assert!(matches!( + err.error, + TypeError::RegionLinearNotConsumed { ref var, .. } if var == "conn" + )); + } + #[test] fn failed_let_restores_outer_binding() { let mut tc = TypeChecker::new(); diff --git a/src/ephapax-wasm/src/debug.rs b/src/ephapax-wasm/src/debug.rs index 4eb50f3..f2b2f47 100644 --- a/src/ephapax-wasm/src/debug.rs +++ b/src/ephapax-wasm/src/debug.rs @@ -35,7 +35,8 @@ pub struct DebugInfo { /// Source file path pub file_path: String, - /// Compilation mode (affine or linear) + /// Module-level mode label. The compiler writes `per-binding`: each + /// binding's own discipline is in `variables`. pub mode: String, /// Mapping from WASM instruction offset to source span diff --git a/src/ephapax-wasm/src/lib.rs b/src/ephapax-wasm/src/lib.rs index 5924f7d..afebebc 100644 --- a/src/ephapax-wasm/src/lib.rs +++ b/src/ephapax-wasm/src/lib.rs @@ -287,6 +287,8 @@ pub fn validate_supported(module: &AstModule) -> Result<(), CodegenError> { /// - `Ty::Borrow { mutable: false, .. }` → `SharedBorrow` /// - `Ty::Ref { linearity: Linear, .. }`, `Ty::String(_)` → `Linear` /// - `Ty::Region { inner, .. }`, `Ty::ForAll { body, .. }` → recurse +/// - any other type that `Ty::is_linear` accepts (a product, sum, tuple or +/// list with a linear component) → `Linear` /// - everything else → `Unrestricted` fn ty_to_ownership_kind(ty: &Ty) -> ownership::OwnershipKind { match ty { @@ -299,6 +301,9 @@ fn ty_to_ownership_kind(ty: &Ty) -> ownership::OwnershipKind { Ty::String(_) => ownership::OwnershipKind::Linear, Ty::Region { inner, .. } => ty_to_ownership_kind(inner), Ty::ForAll { body, .. } => ty_to_ownership_kind(body), + // A product, sum, tuple or list holding a linear component is + // linear in the checker (`Ty::is_linear` join); keep them in step. + _ if ty.is_linear() => ownership::OwnershipKind::Linear, _ => ownership::OwnershipKind::Unrestricted, } } @@ -673,7 +678,9 @@ impl Codegen { /// Enable debug information generation pub fn enable_debug(&mut self, file_path: String) { - let mode_str = "linear".to_string(); + // Discipline is per binding (`let` affine, `let!` linear); there is + // no module-wide mode, so the section must not claim one. + let mode_str = "per-binding".to_string(); self.debug_info = Some(DebugInfo { file_path, @@ -1512,10 +1519,17 @@ impl Codegen { } fn emit_data_section(&mut self) { - if self.data_entries.is_empty() { - return; - } let mut data = wasm_encoder::DataSection::new(); + // The bump pointer lives at `mem[0]`. Start it past the metadata + // area and the string data; left at 0, the first heap cell would + // be allocated over the bump pointer itself (nested pairs read back + // corrupted fields, e.g. `fst((1, (2, 3)))`). + let heap_start = self.data_offset.max(1024).next_multiple_of(8); + data.active( + 0, + &wasm_encoder::ConstExpr::i32_const(0), + heap_start.to_le_bytes(), + ); for (offset, bytes) in &self.data_entries { data.active( 0, @@ -2177,6 +2191,9 @@ impl Codegen { ValType::I32 } ExprKind::If { then_branch, .. } => self.infer_expr_valtype(then_branch), + ExprKind::Match { arms, .. } => arms + .first() + .map_or(ValType::I32, |arm| self.infer_expr_valtype(&arm.body)), ExprKind::Region { body, .. } | ExprKind::Copy(body) | ExprKind::Deref(body) => { self.infer_expr_valtype(body) } @@ -2785,8 +2802,13 @@ impl Codegen { func.instruction(&Instruction::LocalSet(payload_local)); } - // Linear arm dispatch. - func.instruction(&Instruction::Block(BlockType::Result(ValType::I32))); + // Linear arm dispatch. The `$done` block carries the arms' result + // type: every arm has the same type, so the first arm decides it + // (an `I64` extern call in a pair destructuring, for example). + let result_ty = arms + .first() + .map_or(ValType::I32, |arm| self.infer_expr_valtype(&arm.body)); + func.instruction(&Instruction::Block(BlockType::Result(result_ty))); for arm in arms.iter() { // Open $arm_i_fail block. Inside, depth 0 = $arm_i_fail, @@ -2828,7 +2850,7 @@ impl Codegen { // No arm matched at runtime — the typechecker rejects non- // exhaustive matches, so this is dead code for well-typed - // programs. Required to satisfy the $done block's i32 result. + // programs. Required to satisfy the $done block's result type. func.instruction(&Instruction::Unreachable); func.instruction(&Instruction::End); // close $done } @@ -5364,6 +5386,28 @@ mod tests { assert_eq!(entries[0].ret_kind, ownership::OwnershipKind::Unrestricted); } + #[test] + fn ownership_kind_follows_compound_linearity() { + // A pair holding a linear string is linear in the checker, so the + // ownership section must not call it Unrestricted. + let linear_pair = Ty::Prod { + left: Box::new(Ty::String("r".into())), + right: Box::new(Ty::Base(BaseTy::I32)), + }; + assert_eq!( + ty_to_ownership_kind(&linear_pair), + ownership::OwnershipKind::Linear + ); + let plain_pair = Ty::Prod { + left: Box::new(Ty::Base(BaseTy::I32)), + right: Box::new(Ty::Base(BaseTy::I32)), + }; + assert_eq!( + ty_to_ownership_kind(&plain_pair), + ownership::OwnershipKind::Unrestricted + ); + } + #[test] fn ownership_section_omitted_when_no_linear_params() { // fn add(a: I32, b: I32) -> I32 = a + b diff --git a/stdlib/Coproc.eph b/stdlib/Coproc.eph index 60f07e8..9374bb6 100644 --- a/stdlib/Coproc.eph +++ b/stdlib/Coproc.eph @@ -18,10 +18,13 @@ // them. ephapax reaches them through the typed `extern "ephapax_coproc"` // block below. Raw variadic `__ffi(...)` is rejected since v2 plan rung E1: // a typed `extern` declaration is the only FFI path. This is a minimal -// migration that restates the old call sites (the capability tag stays I32 -// and is widened to the shim's i64 at the boundary, as before). The full -// port, including the linearity annotation on `CoprocBuf`, the wire types -// and a host binding for the `ephapax_coproc` import module, is rung E10. +// migration that restates the old call sites. The capability tag stays I32 +// because the language has no I64 literal or I32-to-I64 conversion yet, but +// the shim exports `cap_tag: i64`. Nothing widens it today: a host that +// registers the Zig exports directly fails at instantiation on these two +// imports. The full port, including the tag width (or a widening host +// adapter), the linearity annotation on `CoprocBuf`, the wire types and a +// host binding for the `ephapax_coproc` import module, is rung E10. // The interpreter's `-L` / `[ffi:stub]` path served only raw `__ffi`, so // it no longer reaches this seam. @@ -68,9 +71,9 @@ pub data CoprocBuf = // The stable i64 tag for the shim's dispatch switch. Kept explicit (not // reliant on the runtime's sum-discriminant encoding) so the wire contract // survives any change to how nullary variants are represented: Audio=0 .. -// Fpga=9. (Returns I32 — integer literals default to I32; the extern -// declarations take the tag as I32, widened to the i64 C ABI at the -// boundary.) +// Fpga=9. (Returns I32: integer literals are I32 and there is no I64 +// literal yet. The extern declarations therefore take the tag as I32, +// which does NOT match the shim's i64 parameter; see the header, rung E10.) fn cap_tag(c: Capability): I32 = match c of | Audio => 0 diff --git a/tools/coproc/README.adoc b/tools/coproc/README.adoc index 633bb5e..64c9b89 100644 --- a/tools/coproc/README.adoc +++ b/tools/coproc/README.adoc @@ -55,7 +55,8 @@ Re-wiring the seam through `extern` is rung E10. zig build-lib -dynamic -O ReleaseSafe \ tools/coproc/ephapax_coproc.zig -femit-bin=libephapax_coproc.so -# Run an ephapax program that uses Coproc, with the seam linked: +# HISTORICAL (pre-E1, raw `__ffi` only). This no longer reaches the shim; +# do not use it to test typed Coproc dispatch until rung E10: ephapax run myprog.eph -L libephapax_coproc.so ----