From afcd756d6dd177000593dbf6c95b1f6303fff16d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 11:11:01 +0000 Subject: [PATCH 1/2] fix(ci): repoint the six standards callers to metadatastician/standards Since 2026-10-26 every workflow that calls a standards reusable (Rust CI, Governance, Mirror, Hypatia, Secret Scanner, Scorecard) has failed at startup with zero jobs. hyperpolymath/standards moved to metadatastician/standards, and GitHub does not follow the repository redirect for `jobs..uses` (standards#1442). The old pin 571cc734 also predates standards#1367, so the transfer playbook says it cannot be kept. - All six callers now pin metadatastician/standards at be39f900 (main HEAD). At that commit every `uses:` in the six reusables has a record in standards' own actions.lock (checked from the parsed YAML). - actions.lock: the standards record moves to the new owner (owner_id 297579217, same repo_id) and lists the actions the six reusables use at be39f900. Seven records are added from standards' lock; six records that only the old pin used are removed. scripts/check-lock-sync.sh passes: transitively closed, 0 dangling edges. - scorecard.yml: the job grants `actions: read`, which the callee asks for. Job-level permissions replace the top-level set. - secret-scanner.yml: `secrets: inherit` is removed; the callee declares no secrets (standards#1108 6b). - mirror.yml: `secrets: inherit` is replaced by the ten named secrets the callee declares. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0136P7ZnmBahbhfkFEP6ark4 --- .github/workflows/actions.lock | 77 +++++++++++++++------------- .github/workflows/governance.yml | 2 +- .github/workflows/hypatia-scan.yml | 2 +- .github/workflows/mirror.yml | 15 +++++- .github/workflows/rust-ci.yml | 2 +- .github/workflows/scorecard.yml | 3 +- .github/workflows/secret-scanner.yml | 3 +- 7 files changed, 61 insertions(+), 43 deletions(-) diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index a551f468..4d805c03 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 79ba1dd6..36ed8539 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 af22c72f..4edfda3b 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 5440e468..7080a27a 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 c6ff4640..0e942a1d 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 f2ec1f8b..08678f19 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 80b7c85a..a7027d78 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 From 591db60aff895959cf681fcbaa8b6a6f4611ae5b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 11:11:01 +0000 Subject: [PATCH 2/2] fix: close the open review findings left on #425 PR #425 merged with 28 unresolved bot review threads, and two bot commits landed minutes before the merge without CI (Rust CI was dead, see the previous commit). Most threads were already fixed by those commits; this change fixes the rest that still reproduce. Code: - wasm: `compile_match` opened every match block as `i32`. A pair destructuring whose body returns I64 (an extern call) type-checked but `compile` emitted invalid wasm. The block now takes the first arm's type, and `infer_expr_valtype` knows `Match`. - wasm: the bump pointer at mem[0] was never initialised, so the first heap cell was allocated over it. Nested pairs and three-field constructors read back corrupt fields (`T(1, 2, 3)` gave 167772701, not 321). A data segment now sets it past the metadata and string data. - wasm: `ty_to_ownership_kind` now maps a product, sum, tuple or list with a linear component to Linear, matching `Ty::is_linear`. - wasm: `--debug` wrote `"mode": "linear"` for every module. It now writes `per-binding`, because discipline is per binding. - interp: remove `find_ffi_symbol` and `value_to_i64_with_cstrings`, left unused when raw `__ffi` evaluation was removed (clippy warning). - probe-battery.sh: require GNU timeout (BusyBox reports expiry as 143), escalate to SIGKILL 5 s after the deadline, and keep that as `timeout`. Docs: Coproc.eph said the I32 capability tag is widened to the shim's i64; nothing widens it. The comments now say so (fix is rung E10). The README marks the `ephapax run -L` command as historical. Tests: three-element tuple projection, the I64 pair destructuring, the three-field constructor's runtime value, raw FFI under a `let`, region exit accounting, and compound ownership kinds. TEST-NEEDS.adoc: 568. Validation: cargo test --workspace 571 passed, 0 failed; clippy 0 warnings; fmt clean; status-gate --tests and --proofs OK; no-False gate self-test 29/29; probe battery 57 SAME, 1 SKIP; probe-battery self-test 4/4. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0136P7ZnmBahbhfkFEP6ark4 --- TEST-NEEDS.adoc | 4 +- scripts/probe-battery.sh | 21 +++++++-- src/ephapax-cli/tests/fail_loud.rs | 33 +++++++++++++ src/ephapax-cli/tests/linearity_holes.rs | 60 ++++++++++++++++++++++-- src/ephapax-interp/src/lib.rs | 60 ------------------------ src/ephapax-typing/src/lib.rs | 22 +++++++++ src/ephapax-wasm/src/debug.rs | 3 +- src/ephapax-wasm/src/lib.rs | 58 ++++++++++++++++++++--- stdlib/Coproc.eph | 17 ++++--- tools/coproc/README.adoc | 3 +- 10 files changed, 194 insertions(+), 87 deletions(-) diff --git a/TEST-NEEDS.adoc b/TEST-NEEDS.adoc index b01faafb..f7a7f041 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 392496df..7b336208 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 65e60976..a52aef22 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 ac35b768..d5203adb 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 a90be8e7..9a6547a5 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 2a9ed858..50452cf9 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 4eb50f38..f2b2f47e 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 5924f7dd..afebebcc 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 60f07e81..9374bb68 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 633bb5e6..64c9b89e 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 ----